diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 52b8550..412df81 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: fewieden custom: ['https://paypal.me/fewieden'] diff --git a/.github/example.jpg b/.github/example.jpg deleted file mode 100644 index 4656fe3..0000000 Binary files a/.github/example.jpg and /dev/null differ diff --git a/.github/example2.jpg b/.github/example2.jpg deleted file mode 100644 index f32dd0f..0000000 Binary files a/.github/example2.jpg and /dev/null differ diff --git a/.github/fuel.png b/.github/fuel.png new file mode 100644 index 0000000..7ecd00a Binary files /dev/null and b/.github/fuel.png differ diff --git a/.github/global.png b/.github/global.png new file mode 100644 index 0000000..5949bbc Binary files /dev/null and b/.github/global.png differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5b6db00 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: build + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master, develop ] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [ 14.x ] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run lint diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..754e5b9 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,15 @@ +name: changelog + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: dangoslen/changelog-enforcer@v1.6.1 + with: + changeLogPath: CHANGELOG.md + skipLabels: Skip Changelog diff --git a/.gitignore b/.gitignore index 2164140..e4160f9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ jspm_packages .node_repl_history docs/ +debug.js +.vscode/launch.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2d80e43..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: node_js -node_js: - - "stable" - - "10" - - "12" - - "14" -script: - - npm run lint -cache: - directories: - - node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index cd71b3f..f470049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # MMM-Fuel Changelog +## [2.2.0] + +Thanks to @TheDuffman85 for his contribution to this release. + +### Added + +* Config option `stationIds` to check prices of specific gas stations (Tankerkönig only) +* Github actions + +### Changed + +* 3rd decimal is now superscripted +* Price and distance values are now localized based on global config option `locale`. + +### Removed + +* Travis-CI integration + ## [2.1.2] ### Added diff --git a/MMM-Fuel.js b/MMM-Fuel.js index 3ff3c63..479a8ff 100644 --- a/MMM-Fuel.js +++ b/MMM-Fuel.js @@ -7,7 +7,7 @@ * @see https://github.com/fewieden/MMM-Fuel */ -/* global google */ +/* global google Module Log config */ /** * @external Module @@ -33,18 +33,6 @@ * @requires external:google */ Module.register('MMM-Fuel', { - /** @member {Object} units - Is used to determine the unit symbol of the global config option units. */ - units: { - imperial: 'ml', - metric: 'km' - }, - - /** @member {Object} currencies - Is used to convert currencies into symbols. */ - currencies: { - AUD: '$', - EUR: '€' - }, - /** @member {boolean} sortByPrice - Flag to switch between sorting (price and distance). */ sortByPrice: true, /** @member {?Interval} interval - Toggles sortByPrice */ @@ -317,12 +305,12 @@ Module.register('MMM-Fuel', { * @function initMap * @description Initializes the map, markers and layers. * - * @param {boolean} success - Only initialize the map if success is truthy. + * @param {Error} error - Only initialize if modal render callback doesn't contain error. * * @returns {void} */ - initMap(success) { - if (!success || this.map) { + initMap(error) { + if (error || this.map) { return; } @@ -410,7 +398,33 @@ Module.register('MMM-Fuel', { return '-'; } - return `${this.config.toFixed ? price.toFixed(2) : price} ${this.currencies[this.priceList.currency]}`; + let prefix = ''; + if (typeof price === 'string') { + prefix = price[0]; + price = parseFloat(price.slice(1)); + } + + const fractionDigits = this.config.toFixed ? 2 : 3; + + const priceParts = new Intl.NumberFormat(config.locale, { + style: 'currency', + currency: this.priceList.currency, + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits + }).formatToParts(price); + + return prefix + priceParts.map(part => { + if (!this.config.toFixed && part.type === 'fraction') { + return part.value.slice(0, -1) + part.value.slice(-1).sup(); + } + + return part.value; + }).join(''); }); + this.nunjucksEnvironment().addFilter('formatDistance', distance => new Intl.NumberFormat(config.locale, { + style: 'unit', + unit: this.priceList.unit, + maximumFractionDigits: 1 + }).format(distance)); } }); diff --git a/README.md b/README.md index 00e51fb..ebb871f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/fewieden/MMM-Fuel/master/LICENSE) [![Build Status](https://travis-ci.org/fewieden/MMM-Fuel.svg?branch=master)](https://travis-ci.org/fewieden/MMM-Fuel) [![Code Climate](https://codeclimate.com/github/fewieden/MMM-Fuel/badges/gpa.svg?style=flat)](https://codeclimate.com/github/fewieden/MMM-Fuel) [![Known Vulnerabilities](https://snyk.io/test/github/fewieden/mmm-fuel/badge.svg)](https://snyk.io/test/github/fewieden/mmm-fuel) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/fewieden/MMM-Fuel/master/LICENSE) ![Build status](https://github.com/fewieden/MMM-Fuel/workflows/build/badge.svg) [![Code Climate](https://codeclimate.com/github/fewieden/MMM-Fuel/badges/gpa.svg?style=flat)](https://codeclimate.com/github/fewieden/MMM-Fuel) [![Known Vulnerabilities](https://snyk.io/test/github/fewieden/mmm-fuel/badge.svg)](https://snyk.io/test/github/fewieden/mmm-fuel) # MMM-Fuel @@ -6,7 +6,7 @@ Gas Station Price Module for MagicMirror2 ## Examples -![](.github/example.jpg) ![](.github/example2.jpg) ![](.github/example3.png) +![](.github/fuel.png) ![](.github/example3.png) ## Dependencies @@ -63,6 +63,16 @@ Gas Station Price Module for MagicMirror2 | `rotateInterval` | `60000` (1 min) | How fast the sorting should be switched between byPrice and byDistance. | | `updateInterval` | `900000` (15 mins) | How often should the data be fetched. **If your value is to small, you risk to get banned from the API provider. I suggest a minimum of 15mins** | +## Global config + +| **Option** | **Default** | **Description** | +| --- | --- | --- | +| `locale` | `undefined` | By default it is using your system settings. You can specify the locale in the global MagicMirror config. Possible values are for e.g.: `'en-US'` or `'de-DE'`. | + +To set a global config you have to set the value in your config.js file inside the MagicMirror project. + +![](.github/global.png) + ### tankerkoenig (Germany only) Read the [Terms of Use](https://creativecommons.tankerkoenig.de/#usage) carefully, especially the restrictions for smart mirrors, @@ -72,7 +82,8 @@ or your API access will be suspended. | --- | --- | --- | | `api_key` | REQUIRED | Get an API key for free access to the data of [tankerkoenig.de](https://creativecommons.tankerkoenig.de/#register). | | `types` | `["diesel"]` | Valid options are `diesel`, `e5` and `e10`. | -| `radius` | `5` | Valid range is 1-25. | +| `radius` | `5` | Valid range is 0-25. Set to 0 to disable. Not required if `stationIds` are provided. | +| `stationIds` | `[]` | Optional array of fuel station ids to fetch instead of the radius. You can only specify a maximum of 10 and you can find the ids [here](https://creativecommons.tankerkoenig.de/TankstellenFinder/index.html). Using radius and station ids in parallel will result in more API calls. If you run into issues increase the `updateInterval`. | ### spritpreisrechner (Austria only) diff --git a/apis/nsw.js b/apis/nsw.js index f937575..6a19b62 100644 --- a/apis/nsw.js +++ b/apis/nsw.js @@ -125,7 +125,7 @@ async function getData() { return { types: ['diesel', 'e5'], - unit: 'km', + unit: 'kilometer', currency: 'AUD', byPrice: stations, byDistance: distance diff --git a/apis/spritpreisrechner.js b/apis/spritpreisrechner.js index 1e99c7b..be1e3c9 100644 --- a/apis/spritpreisrechner.js +++ b/apis/spritpreisrechner.js @@ -180,7 +180,7 @@ async function getData() { return { types: ['diesel', 'e5', 'gas'], - unit: 'km', + unit: 'kilometer', currency: 'EUR', byPrice: stations, byDistance: distance diff --git a/apis/tankerkoenig.js b/apis/tankerkoenig.js index a88bf64..64dcd9a 100644 --- a/apis/tankerkoenig.js +++ b/apis/tankerkoenig.js @@ -13,21 +13,52 @@ */ const fetch = require('node-fetch'); -const BASE_URL = 'https://creativecommons.tankerkoenig.de/json/list.php'; +/** + * @external geolib + * @see https://www.npmjs.com/package/geolib + */ +const geolib = require('geolib'); + +const BASE_URL = 'https://creativecommons.tankerkoenig.de/json'; let config; +let stationInfos; /** - * @function generateUrl + * @function generateRadiusUrl * @description Helper function to generate API request url. * * @returns {string} url */ -function generateUrl() { - return `${BASE_URL}?lat=${config.lat}&lng=${config.lng}&rad=${config.radius}&type=all&apikey=${ +function generateRadiusUrl() { + return `${BASE_URL}/list.php?lat=${config.lat}&lng=${config.lng}&rad=${config.radius}&type=all&apikey=${ config.api_key}&sort=dist`; } +/** + * @function generateStationPricesUrl + * @description Helper function to generate API request url. + * + * @param {string[]} ids - Gas Station IDs + * + * @returns {string} url + */ +function generateStationPricesUrl(ids) { + return `${BASE_URL}/prices.php?ids=${ids.join(',')}&apikey=${config.api_key}`; +} + +/** + * @function generateStationInfoUrl + * @description Helper function to generate API request url. + * + * @param {string} id - Gas Station ID + * + * @returns {string} url + */ +function generateStationInfoUrl(id) { + return `${BASE_URL}/detail.php?id=${id}&apikey=${config.api_key}`; +} + /** * @function sortByPrice * @description Helper function to sort gas stations by price. @@ -47,6 +78,25 @@ function sortByPrice(a, b) { return a[config.sortBy] - b[config.sortBy]; } +/** + * @function sortByDistance + * @description Helper function to sort gas stations by distance. + * + * @param {Object} a - Gas Station + * @param {Object} b - Gas Station + * + * @returns {number} Sorting weight. + */ +function sortByDistance(a, b) { + if (b.dist === 0) { + return Number.MIN_SAFE_INTEGER; + } else if (a.dist === 0) { + return Number.MAX_SAFE_INTEGER; + } + + return a.dist - b.dist; +} + /** * @function filterStations * @description Helper function to filter gas stations. @@ -90,6 +140,125 @@ function normalizeStations(value, index, stations) { /* eslint-enable no-param-reassign */ } +/** + * @function getPricesByRadius + * @description Fetches the prices by radius. + * @async + * + * @returns {Object[]} List of stations in raw format. + */ +async function getPricesByRadius() { + const response = await fetch(generateRadiusUrl()); + const parsedResponse = await response.json(); + + if (!parsedResponse.ok) { + throw new Error('Error no fuel radius prices'); + } + + return parsedResponse.stations; +} + +/** + * @function setStationInfos + * @description Initializes the gas station information. + * @async + * + * @param {Object[]} stationsByRadius - Gas Stations by radius. Used to filter out possible duplicate stations. + * + * @returns {void} + */ +async function setStationInfos(stationsByRadius) { + + // Filter out possible duplicate stations which are included in the radius search. + for (const station of stationsByRadius) { + config.stationIds = config.stationIds.filter(e => e !== station.id); + } + + if (config.stationIds.length > 10) { + console.warn(`MMM-Fuel: You can only ask for a maximum of 10 station prices`); + config.stations = config.stationIds.slice(0, 10); + } + + stationInfos = {}; + + for (const stationId of config.stationIds) { + const response = await fetch(generateStationInfoUrl(stationId)); + const parsedResponse = await response.json(); + + if (!parsedResponse.ok) { + throw new Error('Error no fuel station detail'); + } + + const station = parsedResponse.station; + + const distanceMeters = geolib.getDistance({ + latitude: config.lat, + longitude: config.lng, + }, { + latitude: station.lat, + longitude: station.lng, + }, 100); + + stationInfos[station.id] = { ...station, dist: distanceMeters / 1000 }; + } +} + +/** + * @function getPricing + * @description Helper function to calculate prices for getPricesByStationList. + * @async + * + * @returns {Object} Fuel prices for all types. + */ +function getPricing({ status, ...prices }) { + const pricing = { diesel: -1, e5: -1, e10: -1 }; + + if (status !== 'open') { + return pricing; + } + + for (const type in prices) { + if (prices[type]) { + pricing[type] = prices[type]; + } + } + + return pricing; +} + +/** + * @function getPricesByStationList + * @description Fetches the prices by station ID list. + * @async + * + * @param {Object[]} stationsByRadius - Gas Stations by radius. Used to filter out possible duplicate stations. + * + * @returns {Object[]} List of stations in raw format. + */ +async function getPricesByStationList(stationsByRadius) { + if (!stationInfos) { + await setStationInfos(stationsByRadius); + } + + const response = await fetch(generateStationPricesUrl(Object.keys(stationInfos))); + const parsedResponse = await response.json(); + + if (!parsedResponse.ok) { + throw new Error('Error no fuel station prices'); + } + + const stations = []; + for (const [stationId, info] of Object.entries(parsedResponse.prices)) { + stations.push({ + ...stationInfos[stationId], + isOpen: info.status !== 'closed', + prices: getPricing(info) + }); + } + + return stations; +} + /** * @function getData * @description Performs the data query and processing. @@ -100,27 +269,31 @@ function normalizeStations(value, index, stations) { * @see apis */ async function getData() { - const response = await fetch(generateUrl()); - const parsedResponse = await response.json(); + let stations = []; + if (config.radius > 0) { + stations = stations.concat(await getPricesByRadius()); + } - if (!parsedResponse.ok) { - throw new Error('Error no fuel data'); + if (Array.isArray(config.stationIds)) { + stations = stations.concat(await getPricesByStationList(stations)); } - const stations = parsedResponse.stations.filter(filterStations); + const stationsFiltered = stations.filter(filterStations); + stationsFiltered.forEach(normalizeStations); - stations.forEach(normalizeStations); + const distance = stationsFiltered.slice(0); + distance.sort(sortByDistance); - const price = stations.slice(0); + const price = stationsFiltered.slice(0); price.sort(sortByPrice); return { types: ['diesel', 'e5', 'e10'], - unit: 'km', + unit: 'kilometer', currency: 'EUR', byPrice: price, - byDistance: stations + byDistance: distance }; } diff --git a/package-lock.json b/package-lock.json index e6ca569..60f7dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mmm-fuel", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1540,6 +1540,11 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "geolib": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz", + "integrity": "sha512-sfahBXFcgELdpumDZV5b3KWiINkZxC5myAkLk067UUcTmTXaiE9SWmxMEHztn/Eus4JX6kesHxaIuZlniYgUtg==" + }, "get-intrinsic": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", diff --git a/package.json b/package.json index 3193854..706f595 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmm-fuel", - "version": "2.1.2", + "version": "2.2.0", "description": "Gas Station price Module for MagicMirror2", "scripts": { "lint": "eslint . && stylelint **/*.css", @@ -31,6 +31,7 @@ "dependencies": { "fs-extra": "^9.0.1", "moment": "^2.29.1", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "geolib": "3.3.1" } } diff --git a/templates/MMM-Fuel.njk b/templates/MMM-Fuel.njk index 7c82329..b76fb0b 100644 --- a/templates/MMM-Fuel.njk +++ b/templates/MMM-Fuel.njk @@ -43,11 +43,11 @@ {{ gasStation.name | shortenText | safe }} {% for type in config.types %} {% if includes(priceList.types, type) %} - {{ gasStation.prices[type] | formatPrice }} + {{ gasStation.prices[type] | formatPrice | safe }} {% endif %} {% endfor %} {% if config.showDistance %} - {{ gasStation.distance }} {{ priceList.unit }} + {{ gasStation.distance | formatDistance }} {% endif %} {% if config.open %}