diff --git a/Changelog.md b/Changelog.md index a426d90ffe..56252cd6bb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,6 +24,7 @@ __New Features__ - Adds optional `HVACSizingControl/ManualJInputs/InfiltrationMethod` input to specify which method to use for infiltration design load calculations. - Updates heat pump HERS sizing methodology to better prevent unmet hours in warmer climates. - Misc Manual J design load calculation improvements. + - **Breaking change**: Disaggregates "Walls" into "Above Grade Walls" and "Below Grade Walls" in results_design_load_details.csv output file. - Advanced research features: - Optional input `SimulationControl/AdvancedResearchFeatures/OnOffThermostatDeadbandTemperature` to model on/off thermostat deadband with start-up degradation for single and two speed AC/ASHP systems and time-based realistic staging for two speed AC/ASHP systems. - Optional input `SimulationControl/AdvancedResearchFeatures/HeatPumpBackupCapacityIncrement` to model multi-stage electric backup coils with time-based staging. @@ -31,8 +32,9 @@ __New Features__ - BuildResidentialHPXML measure: - **Breaking change**: Replaced `slab_under_width` and `slab_perimeter_depth` arguments with `slab_under_insulation_width` and `slab_perimeter_insulation_depth` - **Breaking change**: Replaced `schedules_vacancy_periods`, `schedules_power_outage_periods`, and `schedules_power_outage_periods_window_natvent_availability` arguments with `schedules_unavailable_period_types`, `schedules_unavailable_period_dates`, and `schedules_unavailable_period_window_natvent_availabilities`; this improves flexibility for handling more unavailable period types. -- **Breaking change**: Disaggregates "Walls" into "Above Grade Walls" and "Below Grade Walls" in results_design_load_details.csv output file. -- Updates `openei_rates.zip` with the latest residential utility rates from the [OpenEI U.S. Utility Rate database](https://apps.openei.org/USURDB/). +- Utility bill calculations: + - Allows OpenEI URDB tariffs that have $/day fixed charges. + - Updates `openei_rates.zip` with the latest residential utility rates from the [OpenEI U.S. Utility Rate database](https://apps.openei.org/USURDB/). - Adds a warning if the sum of supply/return duct leakage to outside values is very high. __Bugfixes__ diff --git a/ReportUtilityBills/measure.rb b/ReportUtilityBills/measure.rb index 1243cc1016..e2819c9420 100644 --- a/ReportUtilityBills/measure.rb +++ b/ReportUtilityBills/measure.rb @@ -328,10 +328,10 @@ def run(runner, user_arguments) utility_rates, utility_bills = setup_utility_outputs() # Get PV monthly fee - monthly_fee = get_monthly_fee(utility_bill_scenario, @hpxml_buildings) + pv_monthly_fee = get_pv_monthly_fee(utility_bill_scenario, @hpxml_buildings) # Get utility rates - warnings = get_utility_rates(hpxml_path, fuels, utility_rates, utility_bill_scenario, monthly_fee, num_units) + warnings = get_utility_rates(hpxml_path, fuels, utility_rates, utility_bill_scenario, pv_monthly_fee, num_units) if register_warnings(runner, warnings) next end @@ -352,26 +352,26 @@ def run(runner, user_arguments) return true end - # Get the monthly grid connection fee. + # Get the PV monthly grid connection fee. # # @param bill_scenario [HPXML::UtilityBillScenario] HPXML Utility Bill Scenario object # @param hpxml_buildings [HPXML::Buildings] HPXML Buildings object # @return [Double] the sum of the monthly grid connection fees ($) across HPXML Buildings - def get_monthly_fee(bill_scenario, hpxml_buildings) - monthly_fee = 0.0 + def get_pv_monthly_fee(bill_scenario, hpxml_buildings) + pv_monthly_fee = 0.0 if not bill_scenario.pv_monthly_grid_connection_fee_dollars_per_kw.nil? hpxml_buildings.each do |hpxml_bldg| hpxml_bldg.pv_systems.each do |pv_system| max_power_output_kW = UnitConversions.convert(pv_system.max_power_output, 'W', 'kW') - monthly_fee += bill_scenario.pv_monthly_grid_connection_fee_dollars_per_kw * max_power_output_kW - monthly_fee *= hpxml_bldg.building_construction.number_of_units if !hpxml_bldg.building_construction.number_of_units.nil? + pv_monthly_fee += bill_scenario.pv_monthly_grid_connection_fee_dollars_per_kw * max_power_output_kW + pv_monthly_fee *= hpxml_bldg.building_construction.number_of_units if !hpxml_bldg.building_construction.number_of_units.nil? end end elsif not bill_scenario.pv_monthly_grid_connection_fee_dollars.nil? - monthly_fee = bill_scenario.pv_monthly_grid_connection_fee_dollars + pv_monthly_fee = bill_scenario.pv_monthly_grid_connection_fee_dollars end - return monthly_fee + return pv_monthly_fee end # Get monthly timestamps for reporting. @@ -535,21 +535,23 @@ def report_monthly_output_results(runner, args, timestamps, monthly_data, monthl # @param fuels [Hash] Fuel type, is_production => Fuel object # @param utility_rates [Hash] Fuel Type => UtilityRate object # @param bill_scenario [HPXML::UtilityBillScenario] HPXML Utility Bill Scenario object - # @param monthly_fee [Double] the sum of the monthly grid connection fees ($) across HPXML Buildings + # @param pv_monthly_fee [Double] the sum of the monthly grid connection fees ($) across HPXML Buildings # @param num_units [Integer] total number of units represented by the HPXML file # @return [Array] array of warnings - def get_utility_rates(hpxml_path, fuels, utility_rates, bill_scenario, monthly_fee, num_units = 1) + def get_utility_rates(hpxml_path, fuels, utility_rates, bill_scenario, pv_monthly_fee, num_units = 1) warnings = [] utility_rates.each do |fuel_type, rate| next if fuels[[fuel_type, false]].timeseries.sum == 0 if fuel_type == FT::Elec if bill_scenario.elec_tariff_filepath.nil? - rate.fixedmonthlycharge = bill_scenario.elec_fixed_charge - rate.flatratebuy = bill_scenario.elec_marginal_rate + rate.fixed_charge_monthly = bill_scenario.elec_fixed_charge + rate.flat_rate = bill_scenario.elec_marginal_rate else require 'json' + tariff_name = File.basename(bill_scenario.elec_tariff_filepath) + filepath = FilePath.check_path(bill_scenario.elec_tariff_filepath, File.dirname(hpxml_path), 'Tariff File') @@ -558,56 +560,54 @@ def get_utility_rates(hpxml_path, fuels, utility_rates, bill_scenario, monthly_f tariff = tariff[:items][0] fields = tariff.keys - rate.fixedmonthlycharge = 0.0 - if fields.include?(:fixedchargeunits) + rate.fixed_charge_monthly = 0.0 + rate.fixed_charge_daily = 0.0 + if fields.include?(:fixedchargeunits) && tariff[:fixedchargefirstmeter].to_f > 0 if tariff[:fixedchargeunits] == '$/month' - rate.fixedmonthlycharge += tariff[:fixedchargefirstmeter] if fields.include?(:fixedchargefirstmeter) - rate.fixedmonthlycharge += tariff[:fixedchargeeaaddl] if fields.include?(:fixedchargeeaaddl) + rate.fixed_charge_monthly += tariff[:fixedchargefirstmeter].to_f + elsif tariff[:fixedchargeunits] == '$/day' + rate.fixed_charge_daily += tariff[:fixedchargefirstmeter].to_f else - warnings << 'Fixed charge units must be $/month.' + warnings << "#{tariff_name}: Unsupported fixed charge units (#{tariff[:fixedchargeunits]}); utility bills will not be calculated." end end - if fields.include?(:minchargeunits) + if fields.include?(:minchargeunits) && tariff[:mincharge].to_f > 0 if tariff[:minchargeunits] == '$/month' - rate.minmonthlycharge = tariff[:mincharge] if fields.include?(:mincharge) + rate.min_charge_monthly = tariff[:mincharge].to_f elsif tariff[:minchargeunits] == '$/year' - rate.minannualcharge = tariff[:mincharge] if fields.include?(:mincharge) + rate.min_charge_annual = tariff[:mincharge].to_f else - warnings << 'Min charge units must be either $/month or $/year.' + warnings << "#{tariff_name}: Unsupported min charge units (#{tariff[:minchargeunits]}); utility bills will not be calculated." end end if fields.include?(:realtimepricing) - rate.realtimeprice = tariff[:realtimepricing] + rate.real_time_prices = tariff[:realtimepricing] else if !fields.include?(:energyweekdayschedule) || !fields.include?(:energyweekendschedule) || !fields.include?(:energyratestructure) - warnings << 'Tariff file must contain energyweekdayschedule, energyweekendschedule, and energyratestructure fields.' + warnings << "#{tariff_name}: Tariff file must contain energyweekdayschedule, energyweekendschedule, and energyratestructure fields; utility bills will not be calculated." end if fields.include?(:demandweekdayschedule) || fields.include?(:demandweekendschedule) || fields.include?(:demandratestructure) || fields.include?(:flatdemandstructure) - warnings << 'Demand charges are not currently supported when calculating detailed utility bills.' + warnings << "#{tariff_name}: Demand charges are not currently supported; utility bills will not be calculated." end - rate.energyratestructure = tariff[:energyratestructure] - rate.energyweekdayschedule = tariff[:energyweekdayschedule] - rate.energyweekendschedule = tariff[:energyweekendschedule] - - if rate.energyratestructure.collect { |r| r.collect { |s| s.keys.include?(:rate) } }.flatten.any? { |t| !t } - warnings << 'Every tier must contain a rate.' - end + rate.energy_rate_structure = tariff[:energyratestructure] + rate.energy_weekday_schedule = tariff[:energyweekdayschedule] + rate.energy_weekend_schedule = tariff[:energyweekendschedule] - if rate.energyratestructure.collect { |r| r.collect { |s| s.keys } }.flatten.uniq.include?(:sell) - warnings << 'No tier may contain a sell key.' + if rate.energy_rate_structure.collect { |r| r.collect { |s| !s.keys.include?(:rate) } }.flatten.any? + warnings << "#{tariff_name}: Every tier must contain a rate; utility bills will not be calculated." end - if rate.energyratestructure.collect { |r| r.collect { |s| s.keys.include?(:unit) } }.flatten.any? { |t| !t } - warnings << 'Every tier must contain a unit' + if rate.energy_rate_structure.collect { |r| r.collect { |s| s.keys } }.flatten.uniq.include?(:sell) + warnings << "#{tariff_name}: Tariffs with sell rates are not currently supported; utility bills will not be calculated." end - if rate.energyratestructure.collect { |r| r.collect { |s| s[:unit] == 'kWh' } }.flatten.any? { |t| !t } - warnings << 'All rates must be in units of kWh.' + if rate.energy_rate_structure.collect { |r| r.collect { |s| s[:unit] != 'kWh' && s.keys.include?(:max) } }.flatten.any? + warnings << "#{tariff_name}: Only max usage units of kWh are currently supported; utility bills will not be calculated." end end end @@ -619,32 +619,33 @@ def get_utility_rates(hpxml_path, fuels, utility_rates, bill_scenario, monthly_f # Feed-In Tariff rate.feed_in_tariff_rate = bill_scenario.pv_feed_in_tariff_rate if bill_scenario.pv_compensation_type == HPXML::PVCompensationTypeFeedInTariff elsif fuel_type == FT::Gas - rate.fixedmonthlycharge = bill_scenario.natural_gas_fixed_charge - rate.flatratebuy = bill_scenario.natural_gas_marginal_rate + rate.fixed_charge_monthly = bill_scenario.natural_gas_fixed_charge + rate.flat_rate = bill_scenario.natural_gas_marginal_rate elsif fuel_type == FT::Oil - rate.fixedmonthlycharge = bill_scenario.fuel_oil_fixed_charge - rate.flatratebuy = bill_scenario.fuel_oil_marginal_rate + rate.fixed_charge_monthly = bill_scenario.fuel_oil_fixed_charge + rate.flat_rate = bill_scenario.fuel_oil_marginal_rate elsif fuel_type == FT::Propane - rate.fixedmonthlycharge = bill_scenario.propane_fixed_charge - rate.flatratebuy = bill_scenario.propane_marginal_rate + rate.fixed_charge_monthly = bill_scenario.propane_fixed_charge + rate.flat_rate = bill_scenario.propane_marginal_rate elsif fuel_type == FT::WoodCord - rate.fixedmonthlycharge = bill_scenario.wood_fixed_charge - rate.flatratebuy = bill_scenario.wood_marginal_rate + rate.fixed_charge_monthly = bill_scenario.wood_fixed_charge + rate.flat_rate = bill_scenario.wood_marginal_rate elsif fuel_type == FT::WoodPellets - rate.fixedmonthlycharge = bill_scenario.wood_pellets_fixed_charge - rate.flatratebuy = bill_scenario.wood_pellets_marginal_rate + rate.fixed_charge_monthly = bill_scenario.wood_pellets_fixed_charge + rate.flat_rate = bill_scenario.wood_pellets_marginal_rate elsif fuel_type == FT::Coal - rate.fixedmonthlycharge = bill_scenario.coal_fixed_charge - rate.flatratebuy = bill_scenario.coal_marginal_rate + rate.fixed_charge_monthly = bill_scenario.coal_fixed_charge + rate.flat_rate = bill_scenario.coal_marginal_rate end - rate.fixedmonthlycharge *= num_units if !rate.fixedmonthlycharge.nil? + rate.fixed_charge_monthly *= num_units if !rate.fixed_charge_monthly.nil? + rate.fixed_charge_daily *= num_units if !rate.fixed_charge_daily.nil? - warnings << "Could not find a marginal #{fuel_type} rate." if rate.flatratebuy.nil? + warnings << "Could not find a marginal #{fuel_type} rate." if rate.flat_rate.nil? # Grid connection fee next unless fuel_type == FT::Elec - rate.fixedmonthlycharge += monthly_fee + rate.fixed_charge_monthly += pv_monthly_fee end return warnings end diff --git a/ReportUtilityBills/measure.xml b/ReportUtilityBills/measure.xml index b0d62111aa..b4b45597c3 100644 --- a/ReportUtilityBills/measure.xml +++ b/ReportUtilityBills/measure.xml @@ -3,8 +3,8 @@ 3.1 report_utility_bills ca88a425-e59a-4bc4-af51-c7e7d1e960fe - 262fad2e-e205-42f3-b54d-79e1b3b2ee8e - 2024-10-13T19:48:31Z + ec7741e4-75b1-4c66-b40a-21ef0e5aa8f1 + 2024-10-16T22:56:39Z 15BF4E57 ReportUtilityBills Utility Bills Report @@ -180,19 +180,19 @@ measure.rb rb script - 007104C8 + 4D23174A - detailed_rates/Adams Electric Cooperative Inc - Rate Schedule T1 TOD (Effective 2013-02-01).json - json + detailed_rates/README.md + md resource - 8E644347 + 4BA8526F - detailed_rates/README.md - md + detailed_rates/Sample Flat Rate Fixed Daily Charge.json + json resource - D038669F + 03F1B3AD detailed_rates/Sample Flat Rate Min Annual Charge.json @@ -294,7 +294,7 @@ detailed_rates/openei_rates.zip zip resource - F41F4AA4 + FCDE5F5D simple_rates/HouseholdConsumption.csv @@ -318,7 +318,7 @@ util.rb rb resource - 86D22906 + 22F928DB Contains Demand Charges.json @@ -360,7 +360,7 @@ test_report_utility_bills.rb rb test - B9C13CA4 + B5EF620B diff --git a/ReportUtilityBills/resources/detailed_rates/README.md b/ReportUtilityBills/resources/detailed_rates/README.md index 1a18534775..1a275bf00f 100644 --- a/ReportUtilityBills/resources/detailed_rates/README.md +++ b/ReportUtilityBills/resources/detailed_rates/README.md @@ -1,5 +1,3 @@ The **openei_rates.zip** file is produced by running `openstudio tasks.rb download_utility_rates`. Rates sourced from the [OpenEI U.S. Utility Rate database](https://apps.openei.org/USURDB/). - -Last updated on 9/18/2024. \ No newline at end of file diff --git a/ReportUtilityBills/resources/detailed_rates/Sample Flat Rate Fixed Daily Charge.json b/ReportUtilityBills/resources/detailed_rates/Sample Flat Rate Fixed Daily Charge.json new file mode 100644 index 0000000000..b4dcb8387a --- /dev/null +++ b/ReportUtilityBills/resources/detailed_rates/Sample Flat Rate Fixed Daily Charge.json @@ -0,0 +1,647 @@ +{ + "items": [ + { + "name": "Sample Tiered Rate", + "sector": "Residential", + "description": "Sample file defined by BEopt.", + "fixedchargeunits": "$/day", + "fixedchargefirstmeter": 0.25, + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.1195179675994109, + "unit": "kWh" + } + ] + ] + } + ] +} \ No newline at end of file diff --git a/ReportUtilityBills/resources/detailed_rates/openei_rates.zip b/ReportUtilityBills/resources/detailed_rates/openei_rates.zip index d4adcff3d4..a416a122d6 100644 Binary files a/ReportUtilityBills/resources/detailed_rates/openei_rates.zip and b/ReportUtilityBills/resources/detailed_rates/openei_rates.zip differ diff --git a/ReportUtilityBills/resources/util.rb b/ReportUtilityBills/resources/util.rb index cf6eb3e2df..8c6c53a654 100644 --- a/ReportUtilityBills/resources/util.rb +++ b/ReportUtilityBills/resources/util.rb @@ -15,27 +15,29 @@ def initialize(meters: [], units:) # Object that stores collections of fixed monthly rates, marginal rates, real-time rates, minimum monthly/annual charges, net metering and feed-in tariff information, and detailed tariff file information. class UtilityRate def initialize() - @fixedmonthlycharge = nil - @flatratebuy = 0.0 - @realtimeprice = nil + @flat_rate = 0.0 + @real_time_prices = nil - @minmonthlycharge = 0.0 - @minannualcharge = nil + @fixed_charge_monthly = nil + @fixed_charge_daily = nil + + @min_charge_monthly = 0.0 + @min_charge_annual = nil @net_metering_excess_sellback_type = nil @net_metering_user_excess_sellback_rate = nil @feed_in_tariff_rate = nil - @energyratestructure = [] - @energyweekdayschedule = [] - @energyweekendschedule = [] + @energy_rate_structure = [] + @energy_weekday_schedule = [] + @energy_weekend_schedule = [] end - attr_accessor(:fixedmonthlycharge, :flatratebuy, :realtimeprice, - :minmonthlycharge, :minannualcharge, + attr_accessor(:fixed_charge_monthly, :fixed_charge_daily, :flat_rate, :real_time_prices, + :min_charge_monthly, :min_charge_annual, :net_metering_excess_sellback_type, :net_metering_user_excess_sellback_rate, :feed_in_tariff_rate, - :energyratestructure, :energyweekdayschedule, :energyweekendschedule) + :energy_rate_structure, :energy_weekday_schedule, :energy_weekend_schedule) end # Object that stores collections of monthly/annual/total fixed/energy charges, as well as monthly/annual production credit. @@ -82,7 +84,7 @@ def self.simple(fuel_type, header, fuel_time_series, is_production, rate, bill, if is_production && fuel_type == FT::Elec && rate.feed_in_tariff_rate monthly_fuel_cost[month_ix] = fuel_time_series[month] * rate.feed_in_tariff_rate else - monthly_fuel_cost[month_ix] = fuel_time_series[month] * rate.flatratebuy + monthly_fuel_cost[month_ix] = fuel_time_series[month] * rate.flat_rate end if fuel_type == FT::Elec @@ -97,10 +99,9 @@ def self.simple(fuel_type, header, fuel_time_series, is_production, rate, bill, bill.monthly_production_credit[month_ix] = monthly_fuel_cost[month_ix] else bill.monthly_energy_charge[month_ix] = monthly_fuel_cost[month_ix] - if not rate.fixedmonthlycharge.nil? - prorate_fraction = calculate_monthly_prorate(header, month_ix + 1) - bill.monthly_fixed_charge[month_ix] = rate.fixedmonthlycharge * prorate_fraction - end + monthly_charge = rate.fixed_charge_monthly.to_f + monthly_charge += rate.fixed_charge_daily.to_f * Calendar.num_days_in_months(header.sim_calendar_year)[month_ix] + bill.monthly_fixed_charge[month_ix] = monthly_charge * calculate_monthly_prorate(header, month_ix + 1) end end @@ -149,14 +150,14 @@ def self.detailed_electric(header, fuels, rate, bill) elec_month = [0] * 12 net_elec_month = [0] * 12 - if !rate.realtimeprice.nil? + if !rate.real_time_prices.nil? num_periods = 0 num_tiers = 0 else - num_periods = rate.energyratestructure.size - num_tiers = rate.energyratestructure.map { |period| period.size }.max + num_periods = rate.energy_rate_structure.size + num_tiers = rate.energy_rate_structure.map { |period| period.size }.max - rate.energyratestructure.each do |period| + rate.energy_rate_structure.each do |period| period.each do |tier| tier[:rate] += tier[:adj] if tier.keys.include?(:adj) end @@ -186,15 +187,15 @@ def self.detailed_electric(header, fuels, rate, bill) net_elec_month[month] += net_elec_hour end - if !rate.realtimeprice.nil? + if !rate.real_time_prices.nil? # Real-Time Pricing - bill.monthly_energy_charge[month] += elec_hour * rate.realtimeprice[hour] + bill.monthly_energy_charge[month] += elec_hour * rate.real_time_prices[hour] if has_production if rate.feed_in_tariff_rate production_fit_month[month] += pv_hour * rate.feed_in_tariff_rate else - net_monthly_energy_charge[month] += net_elec_hour * rate.realtimeprice[hour] + net_monthly_energy_charge[month] += net_elec_hour * rate.real_time_prices[hour] end end @@ -203,14 +204,14 @@ def self.detailed_electric(header, fuels, rate, bill) if (num_periods != 0) || (num_tiers != 0) if (1..5).to_a.include?(today.wday) # weekday - sched_rate = rate.energyweekdayschedule[month][hour_day] + sched_rate = rate.energy_weekday_schedule[month][hour_day] else # weekend - sched_rate = rate.energyweekendschedule[month][hour_day] + sched_rate = rate.energy_weekend_schedule[month][hour_day] end end if (num_periods > 1) || (num_tiers > 1) # tiered or TOU - tiers = rate.energyratestructure[sched_rate] + tiers = rate.energy_rate_structure[sched_rate] if num_tiers > 1 @@ -248,7 +249,7 @@ def self.detailed_electric(header, fuels, rate, bill) bill.monthly_energy_charge[month] += elec_hour * tiers[0][:rate] end else # not tiered or TOU - bill.monthly_energy_charge[month] += elec_hour * rate.energyratestructure[0][0][:rate] + bill.monthly_energy_charge[month] += elec_hour * rate.energy_rate_structure[0][0][:rate] end if has_production @@ -302,7 +303,7 @@ def self.detailed_electric(header, fuels, rate, bill) net_monthly_energy_charge[month] += net_elec_hour * tiers[0][:rate] end else # not tiered or TOU - net_monthly_energy_charge[month] += net_elec_hour * rate.energyratestructure[0][0][:rate] + net_monthly_energy_charge[month] += net_elec_hour * rate.energy_rate_structure[0][0][:rate] end end end @@ -311,11 +312,9 @@ def self.detailed_electric(header, fuels, rate, bill) next unless hour_day == 23 # last hour of the day if Calendar.day_end_months(year).include?(today.yday) - if not rate.fixedmonthlycharge.nil? - # If the run period doesn't span the entire month, prorate the fixed charges - prorate_fraction = calculate_monthly_prorate(header, month + 1) - bill.monthly_fixed_charge[month] = rate.fixedmonthlycharge * prorate_fraction - end + monthly_charge = rate.fixed_charge_monthly.to_f + monthly_charge += rate.fixed_charge_daily.to_f * Calendar.num_days_in_months(header.sim_calendar_year)[month] + bill.monthly_fixed_charge[month] = monthly_charge * calculate_monthly_prorate(header, month + 1) if (num_periods > 1) || (num_tiers > 1) # tiered or TOU @@ -323,9 +322,9 @@ def self.detailed_electric(header, fuels, rate, bill) frac_elec_period = [0] * num_periods for period in 0..num_periods - 1 frac_elec_period[period] = elec_period[period] / elec_month[month] - for t in 0..rate.energyratestructure[period].size - 1 + for t in 0..rate.energy_rate_structure[period].size - 1 if t < elec_tier.size - bill.monthly_energy_charge[month] += rate.energyratestructure[period][t][:rate] * frac_elec_period[period] * elec_tier[t] + bill.monthly_energy_charge[month] += rate.energy_rate_structure[period][t][:rate] * frac_elec_period[period] * elec_tier[t] end end end @@ -343,9 +342,9 @@ def self.detailed_electric(header, fuels, rate, bill) net_frac_elec_period = [0] * num_periods for period in 0..num_periods - 1 net_frac_elec_period[period] = net_elec_period[period] / net_elec_month[month] - for t in 0..rate.energyratestructure[period].size - 1 + for t in 0..rate.energy_rate_structure[period].size - 1 if t < net_elec_tier.size - net_monthly_energy_charge[month] += rate.energyratestructure[period][t][:rate] * net_frac_elec_period[period] * net_elec_tier[t] + net_monthly_energy_charge[month] += rate.energy_rate_structure[period][t][:rate] * net_frac_elec_period[period] * net_elec_tier[t] end end end @@ -374,7 +373,7 @@ def self.detailed_electric(header, fuels, rate, bill) if has_production && !rate.feed_in_tariff_rate # Net metering calculations - annual_payments, monthly_min_charges, end_of_year_bill_credit = apply_min_charges(bill.monthly_fixed_charge, net_monthly_energy_charge, rate.minannualcharge, rate.minmonthlycharge) + annual_payments, monthly_min_charges, end_of_year_bill_credit = apply_min_charges(bill.monthly_fixed_charge, net_monthly_energy_charge, rate.min_charge_annual, rate.min_charge_monthly) end_of_year_bill_credit, excess_sellback = apply_excess_sellback(end_of_year_bill_credit, rate.net_metering_excess_sellback_type, rate.net_metering_user_excess_sellback_rate, net_elec_month.sum(0.0)) annual_total_charge_with_pv = annual_payments + end_of_year_bill_credit - excess_sellback @@ -385,16 +384,16 @@ def self.detailed_electric(header, fuels, rate, bill) end else # Either no PV or PV with FIT - if rate.minannualcharge.nil? + if rate.min_charge_annual.nil? for m in 0..11 monthly_bill = bill.monthly_energy_charge[m] + bill.monthly_fixed_charge[m] - if monthly_bill < rate.minmonthlycharge - bill.monthly_fixed_charge[m] += (rate.minmonthlycharge - monthly_bill) + if monthly_bill < rate.min_charge_monthly + bill.monthly_fixed_charge[m] += (rate.min_charge_monthly - monthly_bill) end end else - if annual_total_charge < rate.minannualcharge - bill.monthly_fixed_charge[11] += (rate.minannualcharge - annual_total_charge) + if annual_total_charge < rate.min_charge_annual + bill.monthly_fixed_charge[11] += (rate.min_charge_annual - annual_total_charge) end end end @@ -513,21 +512,22 @@ def process_usurdb(filepath) require 'json' require 'zip' - skip_keywords = true - keywords = ['lighting', - 'lights', - 'private light', - 'yard light', - 'security light', - 'lumens', - 'watt hps', - 'incandescent', - 'halide', - 'lamps', - '[partial]', - 'rider', - 'irrigation', - 'grain'] + skip_keywords = [ + 'lighting', + 'lights', + 'private light', + 'yard light', + 'security light', + 'lumens', + 'watt hps', + 'incandescent', + 'halide', + 'lamps', + '[partial]', + 'rider', + 'irrigation', + 'grain' + ] puts 'Parsing CSV...' rates = CSV.read(filepath, headers: true) @@ -540,16 +540,16 @@ def process_usurdb(filepath) rates.each do |rate| # rates to skip next if rate['sector'] != 'Residential' - next if !rate['enddate'].nil? - next if keywords.any? { |x| rate['name'].downcase.include?(x) } && skip_keywords + next if !rate['enddate'].nil? # Exclude rates that no longer apply + next if skip_keywords.any? { |x| rate['name'].downcase.include?(x) } - # fixed charges - if ['$/day', '$/year'].include?(rate['fixedchargeunits']) + # unhandled fixed charge units + if (not ['$/day', '$/month'].include?(rate['fixedchargeunits'])) && (rate['fixedchargefirstmeter'].to_f > 0) next end - # min charges - if ['$/day'].include?(rate['minchargeunits']) + # unhandled min charge units + if (not ['$/month', '$/year'].include?(rate['minchargeunits'])) && (rate['mincharge'].to_f > 0) next end @@ -614,16 +614,16 @@ def process_usurdb(filepath) next if rate['energyweekdayschedule'].nil? || rate['energyweekendschedule'].nil? || rate['energyratestructure'].nil? # ignore rates without a "rate" key - next if rate['energyratestructure'].collect { |r| r.collect { |s| s.keys.include?('rate') } }.flatten.any? { |t| !t } + next if rate['energyratestructure'].collect { |r| r.collect { |s| !s.keys.include?('rate') } }.flatten.any? # ignore rates with negative "rate" value - next if rate['energyratestructure'].collect { |r| r.collect { |s| s['rate'] >= 0 } }.flatten.any? { |t| !t } + next if rate['energyratestructure'].collect { |r| r.collect { |s| s['rate'] < 0 } }.flatten.any? # ignore rates with a "sell" key next if rate['energyratestructure'].collect { |r| r.collect { |s| s.keys } }.flatten.uniq.include?('sell') - # set rate units to 'kWh' - rate['energyratestructure'].collect { |r| r.collect { |s| s['unit'] = 'kWh' } } + # ignore rates where max usage is provided but max units are not 'kWh' + next if rate['energyratestructure'].collect { |r| r.collect { |s| s['unit'] != 'kWh' && s.keys.include?('max') } }.flatten.any? residential_rates << { 'items' => [rate] } end @@ -634,7 +634,7 @@ def process_usurdb(filepath) rates_dir = File.dirname(filepath) zippath = File.join(rates_dir, 'openei_rates.zip') FileUtils.rm(zippath) - zipcontents = [] + ratepaths = [] Zip::File.open(zippath, create: true) do |zipfile| residential_rates.each do |residential_rate| utility = valid_filename(residential_rate['items'][0]['utility']) @@ -649,16 +649,16 @@ def process_usurdb(filepath) json = JSON.pretty_generate(residential_rate) f.write(json) end - zipname = File.basename(ratepath) - next if zipcontents.include?(zipname) + next if ratepaths.include?(ratepath) - zipfile.add(zipname, ratepath) - zipcontents << zipname + zipfile.add(File.basename(ratepath), ratepath) + ratepaths << ratepath end end - num_rates_actual = Dir[File.join(rates_dir, '*.json')].count - FileUtils.rm(Dir[File.join(rates_dir, '*.json')]) + ratepaths.each do |ratepath| + FileUtils.rm(ratepath) + end - return num_rates_actual + return ratepaths.count end diff --git a/ReportUtilityBills/tests/test_report_utility_bills.rb b/ReportUtilityBills/tests/test_report_utility_bills.rb index 61558197b8..cb10675745 100644 --- a/ReportUtilityBills/tests/test_report_utility_bills.rb +++ b/ReportUtilityBills/tests/test_report_utility_bills.rb @@ -288,10 +288,10 @@ def test_auto_marginal_rate # Check that we can successfully look up "auto" rates for every state and every fuel type. Constants::StateCodesMap.keys.each do |state_code| fuel_types.each do |fuel_type| - flatratebuy, average_rate = UtilityBills.get_rates_from_eia_data(nil, state_code, fuel_type, 1) # fixed_charge > 0 ensures marginal_rate != average_rate - refute_nil(flatratebuy) + flat_rate, average_rate = UtilityBills.get_rates_from_eia_data(nil, state_code, fuel_type, 1) # fixed_charge > 0 ensures marginal_rate != average_rate + refute_nil(flat_rate) if [HPXML::FuelTypeElectricity, HPXML::FuelTypeNaturalGas].include? fuel_type - assert_operator(flatratebuy, :<, average_rate) + assert_operator(flat_rate, :<, average_rate) else assert_nil(average_rate) end @@ -300,10 +300,10 @@ def test_auto_marginal_rate # Check that we can successfully look up "auto" rates for the US too. fuel_types.each do |fuel_type| - flatratebuy, average_rate = UtilityBills.get_rates_from_eia_data(nil, 'US', fuel_type, 1) # fixed_charge > 0 ensures marginal_rate != average_rate - refute_nil(flatratebuy) + flat_rate, average_rate = UtilityBills.get_rates_from_eia_data(nil, 'US', fuel_type, 1) # fixed_charge > 0 ensures marginal_rate != average_rate + refute_nil(flat_rate) if [HPXML::FuelTypeElectricity, HPXML::FuelTypeNaturalGas].include? fuel_type - assert_operator(flatratebuy, :<, average_rate) + assert_operator(flat_rate, :<, average_rate) else assert_nil(average_rate) end @@ -322,10 +322,10 @@ def test_specified_marginal_rate # Check that we can successfully provide rates for every state and every fuel type. Constants::StateCodesMap.keys.each do |state_code| fuel_types.each do |fuel_type| - flatratebuy, average_rate = UtilityBills.get_rates_from_eia_data(nil, state_code, fuel_type, 1, marginal_rate) # fixed_charge > 0 ensures marginal_rate != average_rate - assert_equal(flatratebuy, marginal_rate) + flat_rate, average_rate = UtilityBills.get_rates_from_eia_data(nil, state_code, fuel_type, 1, marginal_rate) # fixed_charge > 0 ensures marginal_rate != average_rate + assert_equal(flat_rate, marginal_rate) if [HPXML::FuelTypeElectricity, HPXML::FuelTypeNaturalGas].include? fuel_type - assert_operator(flatratebuy, :<, average_rate) + assert_operator(flat_rate, :<, average_rate) else assert_nil(average_rate) end @@ -334,10 +334,10 @@ def test_specified_marginal_rate # Check that we can successfully provide rates for the US too. fuel_types.each do |fuel_type| - flatratebuy, average_rate = UtilityBills.get_rates_from_eia_data(nil, 'US', fuel_type, 1, marginal_rate) # fixed_charge > 0 ensures marginal_rate != average_rate - assert_equal(flatratebuy, marginal_rate) + flat_rate, average_rate = UtilityBills.get_rates_from_eia_data(nil, 'US', fuel_type, 1, marginal_rate) # fixed_charge > 0 ensures marginal_rate != average_rate + assert_equal(flat_rate, marginal_rate) if [HPXML::FuelTypeElectricity, HPXML::FuelTypeNaturalGas].include? fuel_type - assert_operator(flatratebuy, :<, average_rate) + assert_operator(flat_rate, :<, average_rate) else assert_nil(average_rate) end @@ -377,7 +377,7 @@ def test_warning_invalid_fixed_charge_units hpxml = HPXML.new(hpxml_path: File.join(@sample_files_path, 'base.xml')) hpxml.header.utility_bill_scenarios.add(name: 'Test 1', elec_tariff_filepath: '../../ReportUtilityBills/tests/Invalid Fixed Charge Units.json') XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) - expected_warnings = ['Fixed charge units must be $/month.'] + expected_warnings = ['Unsupported fixed charge units ($/year)'] actual_bills, _actual_monthly_bills = _test_measure(expected_warnings: expected_warnings) assert_nil(actual_bills) end @@ -387,7 +387,7 @@ def test_warning_invalid_min_charge_units hpxml = HPXML.new(hpxml_path: File.join(@sample_files_path, 'base.xml')) hpxml.header.utility_bill_scenarios.add(name: 'Test 1', elec_tariff_filepath: '../../ReportUtilityBills/tests/Invalid Min Charge Units.json') XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) - expected_warnings = ['Min charge units must be either $/month or $/year.'] + expected_warnings = ['Unsupported min charge units ($/day)'] actual_bills, _actual_monthly_bills = _test_measure(expected_warnings: expected_warnings) assert_nil(actual_bills) end @@ -397,7 +397,7 @@ def test_warning_demand_charges hpxml = HPXML.new(hpxml_path: File.join(@sample_files_path, 'base.xml')) hpxml.header.utility_bill_scenarios.add(name: 'Test 1', elec_tariff_filepath: '../../ReportUtilityBills/tests/Contains Demand Charges.json') XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) - expected_warnings = ['Demand charges are not currently supported when calculating detailed utility bills.'] + expected_warnings = ['Demand charges are not currently supported'] actual_bills, _actual_monthly_bills = _test_measure(expected_warnings: expected_warnings) assert_nil(actual_bills) end @@ -407,7 +407,7 @@ def test_warning_missing_required_fields hpxml = HPXML.new(hpxml_path: File.join(@sample_files_path, 'base.xml')) hpxml.header.utility_bill_scenarios.add(name: 'Test 1', elec_tariff_filepath: '../../ReportUtilityBills/tests/Missing Required Fields.json') XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) - expected_warnings = ['Tariff file must contain energyweekdayschedule, energyweekendschedule, and energyratestructure fields.'] + expected_warnings = ['Tariff file must contain energyweekdayschedule, energyweekendschedule, and energyratestructure fields'] actual_bills, _actual_monthly_bills = _test_measure(expected_warnings: expected_warnings) assert_nil(actual_bills) end @@ -471,6 +471,16 @@ def test_detailed_flat_pv_none _check_monthly_bills(actual_bills, actual_monthly_bills) end + def test_detailed_flat_pv_none_fixed_daily_charge + @hpxml_header.utility_bill_scenarios[-1].elec_tariff_filepath = '../../ReportUtilityBills/resources/detailed_rates/Sample Flat Rate Fixed Daily Charge.json' + utility_bill_scenario = @hpxml_header.utility_bill_scenarios[0] + actual_bills, actual_monthly_bills = _bill_calcs(@fuels_pv_none_detailed, @hpxml_header, @hpxml.buildings, utility_bill_scenario) + @expected_bills['Test: Electricity: Fixed (USD)'] = 91.25 + expected_bills = _get_expected_bills(@expected_bills) + _check_bills(expected_bills, actual_bills) + _check_monthly_bills(actual_bills, actual_monthly_bills) + end + def test_detailed_flat_pv_1kW_net_metering_user_excess_rate @hpxml_header.utility_bill_scenarios[-1].elec_tariff_filepath = '../../ReportUtilityBills/resources/detailed_rates/Sample Flat Rate.json' @hpxml_bldg.pv_systems.each { |pv_system| pv_system.max_power_output = 1000.0 / @hpxml_bldg.pv_systems.size } @@ -1205,8 +1215,8 @@ def _bill_calcs(fuels, header, hpxml_buildings, utility_bill_scenario) args = { output_format: 'csv', include_annual_bills: true, include_monthly_bills: true, register_annual_bills: true, register_monthly_bills: true } utility_rates, utility_bills = @measure.setup_utility_outputs() - monthly_fee = @measure.get_monthly_fee(utility_bill_scenario, hpxml_buildings) - @measure.get_utility_rates(@hpxml_path, fuels, utility_rates, utility_bill_scenario, monthly_fee) + pv_monthly_fee = @measure.get_pv_monthly_fee(utility_bill_scenario, hpxml_buildings) + @measure.get_utility_rates(@hpxml_path, fuels, utility_rates, utility_bill_scenario, pv_monthly_fee) @measure.get_utility_bills(fuels, utility_rates, utility_bills, utility_bill_scenario, header) # Annual