fewieden/MMM-Fuel

View on GitHub
apis/nsw.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * @file apis/nsw.js
 *
 * @author fewieden
 * @license MIT
 *
 * @see  https://github.com/fewieden/MMM-Fuel
 */

/**
 * @external lodash
 * @see https://www.npmjs.com/package/lodash
 */
const _ = require('lodash');

/**
 * @external node-fetch
 * @see https://www.npmjs.com/package/node-fetch
 */
const fetch = require('node-fetch');

/**
 * @external moment
 * @see https://www.npmjs.com/package/moment
 */
const moment = require('moment');

/**
 * @external logger
 * @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js
 */
const Log = require('logger');

const { filterStations } = require('./utils');

const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const QUARTER_DAY = 6 * HOUR;
const BASE_URL = 'https://api.onegov.nsw.gov.au';
const TYPES = {
    diesel: 'DL',
    e5: 'P95'
};

let transaction = 1;
let config;
let token;

/**
 * @function refreshToken
 * @description Helper function to refresh the API token.
 * @async
 *
 * @param {object} config - Config object with authentication details.
 *
 * @returns {Promise} This async function just returns a promise but no value.
 *
 * @see apis/README.md
 */
async function refreshToken(config) {
    try {
        const response = await fetch(`${BASE_URL}/oauth/client_credential/accesstoken?grant_type=client_credentials`, {
            headers: {
                'Content-Type': 'application/json',
                authorization: `Basic ${Buffer.from(`${config.api_key}:${config.secret}`).toString('base64')}`,
                'User-Agent': 'MagicMirror²'
            }
        });
        const parsedResponse = await response.json();

        if (parsedResponse.Error) {
            throw new Error(parsedResponse.Error);
        }

        token = parsedResponse.access_token;
    } catch (e) {
        Log.error('MMM-Fuel: Failed to refresh token', e);
    }
}

/**
 * @function getData
 * @description Performs the data query and processing.
 * @async
 *
 * @returns {Object} Returns object described in the provider documentation.
 *
 * @see apis/README.md
 */
async function getData() {
    const responses = await Promise.all(config.types.map(requestFuelType));
    const collection = {};
    for (const response of responses) {
        collection[response.type] = mapPriceToStation(response.data);
    }

    let stations = collection[config.sortBy];

    const maxPrices = {};
    for (const type in collection) {
        for (const station of collection[type]) {
            if (!maxPrices[type] || station.price > maxPrices[type]) {
                maxPrices[type] = station.price;
            }
        }
    }

    delete collection[config.sortBy];
    const keys = Object.keys(collection);

    normalizeStations(stations, keys);

    keys.forEach(type => {
        collection[type].forEach(station => {
            for (let i = 0; i < stations.length; i += 1) {
                if (station.code === stations[i].code) {
                    stations[i].prices[type] = station.price;
                    break;
                }
            }
        });
    });

    for (const station of stations) {
        for (const type in station.prices) {
            if (station.prices[type] === -1) {
                station.prices[type] = `>${maxPrices[type]}`;
            }
        }
    }

    stations = stations.filter(filterStations);

    return {
        types: ['diesel', 'e5'],
        unit: 'kilometer',
        currency: 'AUD',
        byPrice: stations,
        byDistance: _.sortBy(stations, 'distance')
    };
}

/**
 * @function requestFuelType
 * @description API request for specified type.
 * @async
 *
 * @param {string} type - Fuel type.
 *
 * @returns {Promise} Object with fuel type and data.
 */
async function requestFuelType(type) {
    const response = await fetch(`${BASE_URL}/FuelPriceCheck/v2/fuel/prices/nearby`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            apikey: config.api_key,
            Authorization: `Bearer ${token}`,
            transactionid: transaction++,
            requesttimestamp: moment().utc()
                .format('DD/MM/YYYY hh:mm:ss A')
        },
        body: JSON.stringify({
            fueltype: TYPES[type],
            latitude: `${config.lat}`,
            longitude: `${config.lng}`,
            radius: `${config.radius}`,
            sortby: 'Price',
            sortascending: 'true'
        })
    });

    return {
        type,
        data: await response.json()
    };
}

/**
 * @function normalizeStations
 * @description Helper function to normalize the structure of gas stations for the UI.
 *
 * @param {Object[]} stations - Gas Station.
 * @param {string[]} keys - Fuel types except config option sortBy.
 *
 * @returns {void}
 *
 * @see apis/README.md
 */
function normalizeStations(stations, keys) {
    stations.forEach((value, index) => {
        /* eslint-disable no-param-reassign */
        stations[index].prices = { [config.sortBy]: value.price };
        keys.forEach(type => {
            stations[index].prices[type] = -1;
        });
        stations[index].lat = value.location.latitude;
        stations[index].lng = value.location.longitude;
        stations[index].distance = value.location.distance;
        /* eslint-enable no-param-reassign */
    });
}

/**
 * @function mapPriceToStation
 * @description Helper function to map prices to gas stations.
 *
 * @param {Object} entities - Entities.
 * @param {Object[]} entities.stations - Gas Stations.
 * @param {Object[]} entities.prices - Fuel Prices.
 *
 * @returns {Object[]} Gas Stations.
 *
 * @see apis/README.md
 */
function mapPriceToStation({ stations, prices }) {
    for (const station of stations) {
        for (const price of prices) {
            if (station.code === price.stationcode) {
                station.price = price.price / 100;
                break;
            }
        }
    }

    return stations;
}

/**
 * @module apis/nsw
 * @description Queries data from https://api.nsw.gov.au
 * @async
 *
 * @requires external:node-fetch
 * @requires external:moment
 * @requires external:logger
 * @requires module:Utils
 *
 * @param {Object} options - Configuration.
 * @param {number} options.lat - Latitude of Coordinate.
 * @param {number} options.lng - Longitude of Coordinate.
 * @param {int} options.radius - Lookup area for gas stations.
 * @param {string} options.sortBy - Type to sort by price.
 * @param {string[]} options.types - Requested fuel types.
 * @param {boolean} options.showOpenOnly - Flag to show only open gas stations.
 *
 * @returns {Object} Object with function getData.
 */
module.exports = async options => {
    config = options;

    await refreshToken(config);
    setInterval(() => refreshToken(config), QUARTER_DAY);

    return { getData };
};