BathHacked/energy-sparks

View on GitHub
app/services/schools/advice/baseload_service.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# rubocop:disable Naming/AsciiIdentifiers
module Schools
  module Advice
    class BaseloadService < BaseService
      include AnalysableMixin

      def enough_data?
        baseload_service.enough_data?
      end

      def data_available_from
        baseload_service.data_available_from
      end

      def has_electricity?
        @school.has_electricity?
      end

      def multiple_electricity_meters?
        @school.meters.active.electricity.count > 1
      end

      def average_baseload_kw(period: :year)
        @average_baseloads_period ||= {}
        @average_baseloads_period[period] ||= baseload_service.average_baseload_kw(period: period)
      end

      def previous_period_average_baseload_kw(period: :year)
        case period
        when :year
          baseload_service = Baseload::BaseloadCalculationService.new(aggregate_meter, end_of_previous_year)
          @previous_year_average_baseload_kw ||= baseload_service.enough_data? ? baseload_service.average_baseload_kw(period: period) : nil
        when :week
          baseload_service = Baseload::BaseloadCalculationService.new(aggregate_meter, end_of_previous_week)
          @previous_week_average_baseload_kw ||= baseload_service.enough_data? ? baseload_service.average_baseload_kw(period: period) : nil
        else
          raise 'Invalid period'
        end
      end

      def saving_through_1_kw_reduction_in_baseload
        baseload_service.saving_through_1_kw_reduction_in_baseload
      end

      def annual_baseload_usage
        @annual_baseload_usage ||= baseload_service.annual_baseload_usage(include_percentage: true)
      end

      def average_baseload_kw_benchmark(compare: :benchmark_school)
        benchmark_service.average_baseload_kw(compare: compare)
      end

      def baseload_usage_benchmark(compare: :benchmark_school)
        benchmark_service.baseload_usage(compare: compare)
      end

      def estimated_savings(versus: :benchmark_school)
        benchmark_service.estimated_savings(versus: versus)
      end

      # Calculate the annual average baseload for every year
      def annual_average_baseloads
        start_date = aggregate_meter.amr_data.start_date
        end_date = aggregate_meter.amr_data.end_date
        (start_date.year..end_date.year).map do |year|
          end_of_year = Date.new(year).end_of_year
          baseload_service = Baseload::BaseloadCalculationService.new(aggregate_meter, end_of_year)
          {
            year: year,
            baseload: baseload_service.average_baseload_kw(period: :year),
            baseload_usage: baseload_service.annual_baseload_usage
          }
        end
      end

      def baseload_meter_breakdown
        meter_breakdown_service = Baseload::BaseloadMeterBreakdownService.new(@meter_collection)
        baseloads = meter_breakdown_service.calculate_breakdown
        meter_breakdowns = {}
        baseloads.meters.each do |mpan_mprn|
          baseload_service = Baseload::BaseloadCalculationService.new(@meter_collection.meter?(mpan_mprn), end_of_previous_year)
          previous_year_baseload = baseload_service.enough_data? ? baseload_service.average_baseload_kw(period: :year) : nil
          meter_breakdowns[mpan_mprn] = build_meter_breakdown(mpan_mprn, baseloads, previous_year_baseload)
        end
        meter_breakdowns
      end

      # helper for building "all meters" / total row for meter breakdown table
      def meter_breakdown_table_total
        baseload_usage = annual_baseload_usage
        previous_year_baseload = previous_period_average_baseload_kw(period: :year)
        baseload_kw = average_baseload_kw
        OpenStruct.new(
          baseload_kw: baseload_kw,
          baseload_cost_£: baseload_usage.£,
          percentage_baseload: 1.0,
          baseload_previous_year_kw: previous_year_baseload,
          baseload_change_kw: previous_year_baseload ? baseload_kw - previous_year_baseload : nil
        )
      end

      def seasonal_variation
        calculate_seasonal_variation(aggregate_meter, asof_date)
      end

      def seasonal_variation_by_meter
        return {} unless electricity_meters.count > 1
        electricity_meters.each_with_object({}) do |meter, variation_by_meter|
          variation_by_meter[meter.mpan_mprn] = calculate_seasonal_variation(meter, meter.amr_data.end_date, true)
        end
      end

      def intraweek_variation
        calculate_intraweek_variation(aggregate_meter, asof_date)
      end

      def intraweek_variation_by_meter
        return {} unless electricity_meters.count > 1
        electricity_meters.each_with_object({}) do |meter, variation_by_meter|
          variation_by_meter[meter.mpan_mprn] = calculate_intraweek_variation(meter, meter.amr_data.end_date, true)
        end
      end

      def benchmark_baseload
        average_baseload_kw_last_year = average_baseload_kw(period: :year)
        average_baseload_kw_benchmark = average_baseload_kw_benchmark(compare: :benchmark_school)
        average_baseload_kw_exemplar = average_baseload_kw_benchmark(compare: :exemplar_school)

        Schools::Comparison.new(
          school_value: average_baseload_kw_last_year,
          benchmark_value: average_baseload_kw_benchmark,
          exemplar_value: average_baseload_kw_exemplar,
          unit: :kw
        )
      end

      private

      def asof_date
        @asof_date ||= aggregate_meter.amr_data.end_date
      end

      # the ElectricityBaseloadAnalysis class defines one_week_ago as
      # 6 days before the calculation date. So when we calculate last weeks
      # baseload is asof_date - 6. This is apparently to address some issues
      # with large intraweek variation.
      #
      # So for our comparison of last week and previous week, we want a week
      # before that which is 13 days...
      def end_of_previous_week
        asof_date - 13
      end

      def end_of_previous_year
        @end_of_previous_year ||= asof_date - 1.year
      end

      def electricity_meters
        @electricity_meters ||= @meter_collection.electricity_meters.select { |meter| meter.fuel_type == :electricity }
      end

      def aggregate_meter
        @meter_collection.aggregated_electricity_meters
      end

      def baseload_service
        @baseload_service ||= Baseload::BaseloadCalculationService.new(aggregate_meter, asof_date)
      end

      def benchmark_service
        @benchmark_service ||= Baseload::BaseloadBenchmarkingService.new(@meter_collection, asof_date)
      end

      def build_meter_breakdown(mpan_mprn, breakdown, previous_year_baseload)
        OpenStruct.new(
          meter: meter_for_mpan(mpan_mprn),
          baseload_kw: breakdown.baseload_kw(mpan_mprn),
          baseload_cost_£: breakdown.baseload_cost_£(mpan_mprn),
          percentage_baseload: breakdown.percentage_baseload(mpan_mprn),
          baseload_previous_year_kw: previous_year_baseload,
          baseload_change_kw: previous_year_baseload ? breakdown.baseload_kw(mpan_mprn) - previous_year_baseload : nil
        )
      end

      def calculate_seasonal_variation(analytics_meter = aggregate_meter, date = asof_date, load_meter = false)
        meter = load_meter ? meter_for_mpan(analytics_meter.mpan_mprn) : nil
        seasonal_baseload_service = Baseload::SeasonalBaseloadService.new(analytics_meter, date)
        # return if there's not enough data, then return limited object
        return OpenStruct.new(meter: meter, enough_data?: false, data_available_from: seasonal_baseload_service.data_available_from) unless enough_data_for_meter?(analytics_meter)
        variation = seasonal_baseload_service.seasonal_variation
        # we may have >1 year of data, but not enough to actually calculate a seasonal analysis
        # e.g. a meter for a swimming pool only used in the summer
        return OpenStruct.new(meter: meter, enough_data?: false) if variation.percentage.nan?
        saving = seasonal_baseload_service.estimated_costs
        build_seasonal_variation(meter, variation, saving)
      end

      def build_seasonal_variation(meter, variation, saving)
        OpenStruct.new(
          meter: meter,
          winter_kw: variation.winter_kw,
          summer_kw: variation.summer_kw,
          percentage: variation.percentage,
          estimated_saving_£: saving.£,
          estimated_saving_co2: saving.co2,
          variation_rating: seasonal_variation_rating(variation.percentage),
          enough_data?: true
        )
      end

      def seasonal_variation_rating(percentage)
        calculate_rating_from_range(0, 0.50, percentage)
      end

      def calculate_intraweek_variation(analytics_meter = aggregate_meter, date = asof_date, load_meter = false)
        intraweek_baseload_service = Baseload::IntraweekBaseloadService.new(analytics_meter, date)
        meter = load_meter ? meter_for_mpan(analytics_meter.mpan_mprn) : nil
        return OpenStruct.new(meter: meter, enough_data?: false, data_available_from: intraweek_baseload_service.data_available_from) unless enough_data_for_meter?(analytics_meter)
        variation = intraweek_baseload_service.intraweek_variation
        saving = intraweek_baseload_service.estimated_costs
        build_intraweek_variation(meter, variation, saving)
      end

      def build_intraweek_variation(meter, variation, saving)
        OpenStruct.new(
          meter: meter,
          max_day: variation.max_day,
          min_day: variation.min_day,
          max_day_kw: variation.max_day_kw,
          min_day_kw: variation.min_day_kw,
          percent_intraday_variation: variation.percent_intraday_variation,
          estimated_saving_£: saving.£,
          estimated_saving_co2: saving.co2,
          variation_rating: intraweek_variation_rating(variation.percent_intraday_variation),
          enough_data?: true
        )
      end

      def intraweek_variation_rating(percentage)
        calculate_rating_from_range(0.1, 0.3, percentage)
      end

      def enough_data_for_meter?(analytics_meter)
        Baseload::BaseloadAnalysis.new(analytics_meter).one_years_data?
      end
    end
  end
end
# rubocop:enable Naming/AsciiIdentifiers