BathHacked/energy-sparks

View on GitHub
app/services/amr/n3rgy_downloader.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'dashboard'

module Amr
  class N3rgyDownloader
    KWH_PER_M3_GAS = 11.1 # this depends on the calorifc value of the gas and so is an approximate average

    def initialize(meter:, start_date:, end_date:)
      @meter = meter
      @start_date = start_date
      @end_date = end_date
    end

    def readings
      # Turn the array of [DateTime, value] into a hash
      readings_by_date_time = fetch_all_readings

      # Creates a hash of { readings: {date => x48 array}, missing_readings: [date_time] }
      # The fourth parameter is set to true to ensure that we correctly process the date times
      # in the list of readings. For a given day d, n3rgy return the final half-hourly reading
      # as midnight of d+1. This conversion function handles this, so that the readings
      # are properly associated with each date
      meter_readings = X48Formatter.convert_dt_to_v_to_date_to_v_x48(@start_date.to_date,
        @end_date.to_date, readings_by_date_time, true, nil)

      # This return format matches the original v1 code. This can be simplified as there is no
      # need to create the one day readings to then just throw them away in the next step
      {
        @meter.meter_type =>
          {
            mpan_mprn:        @meter.mpan_mprn,
            readings:         make_one_day_readings(meter_readings[:readings], @meter.mpan_mprn),
            missing_readings: meter_readings[:missing_readings]
          }
      }
    end

    private

    # TODO remove, unnecessary, see note above
    def make_one_day_readings(meter_readings_by_date, mpan_mprn)
      meter_readings_by_date.map do |date, readings|
        [date.to_date, OneDayAMRReading.new(mpan_mprn, date.to_date, 'ORIG', nil, DateTime.now, readings, true)]
      end.to_h
    end

    # Query the n3rgy API in blocks of up to 90 days to fetch all of the readings
    #
    # start_date is a DateTime with hour/mins of 00:30
    # end_date is a DateTime with hour/mins of 00:00
    #
    # `.each_slice` returns ranges that use the same hours/mins as the start of the
    # sliced range. So we need to adjust the end range before use
    #
    # Slices use 89 days because of this
    #
    #
    # Extracts the readings from each API response, adjusting units as required
    #
    # Returns a single hash of DateTime => half hourly reading value
    def fetch_all_readings
      readings = []
      (@start_date..@end_date).each_slice(89) do |date_range_max_90days|
        start_date_time = date_range_max_90days.first
        end_date_time = (date_range_max_90days.last + 1.day).change({ hour: 0, min: 0, sec: 0 })
        response = api_client.readings(@meter.mpan_mprn,
          @meter.fuel_type.to_s,
          DataFeeds::N3rgy::DataApiClient::READING_TYPE_CONSUMPTION,
          start_date_time,
          end_date_time)
        readings += extract_readings(response)
      end
      readings.to_h
    end

    # Convert an n3rgy v2 API response into an array of [DateTime, value] values
    #
    # For gas readings, values are converted from cubic meters to kWh using
    # a fixed conversion value.
    def extract_readings(response)
      return [] unless response.dig('devices', 0, 'values').present?

      # v2 returns an array of readings for each 'device'. It is technically possible
      # within the SMETS standard for there to be up to 5 devices of the same type,
      # e.g. 5 electricity meters. But this is an edge case and in practice they
      # would all have MPANs.
      warn_if_multiple_devices(response)
      #
      # The responses may also contain a secondaryValue which we are also ignoring
      # for the moment. These are for Twin Element Electricity Meters which monitor
      # two circuits.
      #
      # Individual values can also include an additionalInformation key, to indicate
      # why data is missing which we are not currently using either.
      response['devices'][0]['values'].map do |half_hourly_reading|
        value = case response['unit']
                when 'm3'
                  to_kwh(half_hourly_reading['primaryValue'])
                else
                  half_hourly_reading['primaryValue']
                end
        [DateTime.parse(half_hourly_reading['timestamp']), value]
      end
    end

    def to_kwh(value)
      value.nil? ? nil : KWH_PER_M3_GAS * value
    end

    def api_client
      DataFeeds::N3rgy::DataApiClient.production_client
    end

    def warn_if_multiple_devices(response)
      if response['devices'].length > 1
        Rollbar.warning("Multiple devices (#{response['devices'].length}) present in n3rgy readings API",
                      meter: @meter.mpan_mprn,
                      school: @meter.school.name
                    )
      end
    end
  end
end