├── .gitignore ├── MMM-Paris-RATP-PG 2.3.png ├── MMM-Paris-RATP-Transport.css ├── package.json ├── LICENSE ├── node_helper.js ├── README.md └── MMM-Paris-RATP-PG.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.js 3 | -------------------------------------------------------------------------------- /MMM-Paris-RATP-PG 2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/da4throux/MMM-Paris-RATP-PG/HEAD/MMM-Paris-RATP-PG 2.3.png -------------------------------------------------------------------------------- /MMM-Paris-RATP-Transport.css: -------------------------------------------------------------------------------- 1 | .paristransport { 2 | font-size: 75%; 3 | line-height: 65px; 4 | display: inline-block; 5 | -ms-transform: translate(0, -3px); /* IE 9 */ 6 | -webkit-transform: translate(0, -3px); /* Safari */ 7 | transform: translate(0, -3px); 8 | } 9 | 10 | .paristransport table { 11 | width: 400px; 12 | } 13 | 14 | .paristransport .red { 15 | color: #FF0000 16 | } 17 | 18 | .paristransport .velibTrendGraph { 19 | border: solid dimgrey; 20 | border-width: 1px 1px 0 1px; 21 | margin-top: 1ex; 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MMM-Paris-RATP-PG", 3 | "version": "2.3.2", 4 | "description": "RATP Local transport in Paris based on Pierre Grimaud API module for MagicMirror", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/da4throux/MMM-Paris-RATP-PG" 8 | }, 9 | "keywords": [ 10 | "magic mirror", 11 | "smart mirror", 12 | "localtransport", 13 | "module", 14 | "paris", 15 | "RATP" 16 | ], 17 | "author": "da4throux", 18 | "contributors": "https://github.com/da4throux/MMM-Paris-RATP-PG/graphs/contributors", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/da4throux/MMM-Paris-RATP-PG/issues" 22 | }, 23 | "homepage": "https://github.com/da4throux/MMM-Paris-RATP-PG#readme", 24 | "dependencies": { 25 | "unirest": "latest", 26 | "node-forge": "latest" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /* Magic Mirror 2 | * Module: MMM-Paris_RATP-PG 3 | * 4 | * script from da4throux 5 | * based on a Script from Georg Peters (https://lane6.de) 6 | * band a Script from Benjamin Angst http://www.beny.ch 7 | * MIT Licensed. 8 | * 9 | * For the time being just the first bus from the config file 10 | */ 11 | 12 | const NodeHelper = require("node_helper"); 13 | const unirest = require('unirest'); 14 | 15 | module.exports = NodeHelper.create({ 16 | start: function () { 17 | this.started = false; 18 | }, 19 | 20 | socketNotificationReceived: function(notification, payload) { 21 | const self = this; 22 | if (notification === 'SET_CONFIG' && this.started == false) { 23 | this.config = payload; 24 | if (this.config.debug) { 25 | console.log (' *** config received from MMM.js & set in node_helper: '); 26 | console.log ( payload ); 27 | } 28 | this.started = true; 29 | this.config.lines.forEach(function(l){ 30 | setTimeout(function(){ 31 | if (self.config.debug) { 32 | console.log (' *** line ' + l.label + ' initial update in ' + l.initialLoadDelay); 33 | } 34 | self.fetchHandleAPI(l); 35 | }, l.initialLoadDelay); 36 | }); 37 | } 38 | }, 39 | 40 | fetchHandleAPI: function(_l) { 41 | var self = this, _url = _l.url, retry = true; 42 | if (this.config.debug) { console.log (' *** fetching: ' + _url);} 43 | unirest.get(_url) 44 | .header({ 45 | 'Accept': 'application/json;charset=utf-8' 46 | }) 47 | .end(function(response){ 48 | if (response && response.body) { 49 | if (self.config.debug) { 50 | console.log (' *** received answer for: ' + (_l.label || '')); 51 | console.log (JSON.toString(_l)); //**** to clean up 52 | } 53 | switch (_l.type) { 54 | case'pluie': 55 | self.processPluie(response.body, _l); 56 | break; 57 | case 'tramways': 58 | case 'buses': 59 | case 'rers': 60 | case 'metros': 61 | self.processRATP(response.body, _l); 62 | break; 63 | case 'traffic': 64 | self.processTraffic(response.body, _l); 65 | break; 66 | case 'autolib': 67 | self.processAutolib(response.body, _l); 68 | break; 69 | case 'velib': 70 | self.processVelib(response.body, _l); 71 | break; 72 | default: 73 | if (this.config.debug) { 74 | console.log(' *** unknown request: ' + l.type); 75 | } 76 | } 77 | } else { 78 | if (self.config.debug) { 79 | if (response) { 80 | console.log (' *** partial response received for: ' + _l.label); 81 | console.log (response); 82 | } else { 83 | console.log (' *** no response received for: ' + _l.label); 84 | } 85 | } 86 | } 87 | if (self.config.debug) { console.log (' *** getResponse: set retry for ' + _l.label); } 88 | }) 89 | if (retry) { 90 | if (this.config.debug) { 91 | console.log (' *** line ' + _l.label + ' initial update in ' + _l.updateInterval); 92 | } 93 | setTimeout(function() { 94 | self.fetchHandleAPI(_l); 95 | }, _l.updateInterval); 96 | } 97 | }, 98 | 99 | log: function (message) { 100 | if (this.config.debug) { 101 | console.log (message); 102 | } 103 | }, 104 | 105 | orderResult: function (result) { 106 | this.config.reorderPotential++; 107 | let orderChanged = false; 108 | let schedules = result.schedules; 109 | if (schedules) { 110 | schedules.sort( function (objA, objB) { 111 | let dateA, dateB; 112 | let a = objA.message, b = objB.message; 113 | dateA = Date.parse('01/01/2011 ' + a + ':00'); 114 | dateB = Date.parse('01/01/2011 ' + b + ':00'); 115 | if ((a[0] == '2') && (b[0] + b[1] == '00')) { 116 | return -1 117 | } 118 | if ((b[0] == '2') && (a[0] + a[1] == '00')) { 119 | orderChanged = true; 120 | return 1 121 | } 122 | if (dateA > dateB) { 123 | orderChanged = true; 124 | return 1; 125 | } else { 126 | return -1; 127 | } 128 | }); 129 | } 130 | if (orderChanged) { 131 | result.schedules = schedules; 132 | this.config.reordered++; 133 | } 134 | return orderChanged; 135 | }, 136 | 137 | processAutolib: function (data, _l) { 138 | this.config.infos[_l.id].lastUpdate = new Date(); 139 | this.config.infos[_l.id].data = data.records[0].fields; 140 | this.loaded = true; 141 | this.sendSocketNotification("DATA", this.config.infos); 142 | }, 143 | 144 | processVelib: function (data, _l) { 145 | var _p = this.config.infos[_l.id]; 146 | if (data.records) { // else it was missing 147 | _p.lastUpdate = new Date(); 148 | _p.data = data.records[0].fields; 149 | _p.data.nbbike = _p.data.mechanical; 150 | _p.data.nbebike = _p.data.ebike; 151 | _p.data.nbfreeedock = _p.data.numdocksavailable; 152 | _p.data.station_state = _p.data.is_renting == "OUI" ? 'Operative' : 'Closed'; 153 | _p.data.update = new Date(); 154 | this.loaded = true; 155 | this.sendSocketNotification("DATA", this.config.infos); 156 | } 157 | }, 158 | 159 | processPluie: function(data, _l) { 160 | var _p = this.config.infos[_l.id]; 161 | if (this.config.debug) { 162 | console.log(' *** Pluie: ' + JSON.stringify(data)); 163 | } 164 | _p.lastUpdateData = data.lastUpdate; //? useful 165 | _p.lastUpdate = new Date(); 166 | _p.niveauPluieText = data.niveauPluieText; 167 | _p.dataCadran = data.dataCadran; 168 | this.loaded = true; 169 | this.sendSocketNotification("DATA", this.config.infos); 170 | }, 171 | 172 | processRATP: function(data, _l) { 173 | this.log (' *** processRATP data received for ' + (_l.label || '')); 174 | if (this.config.reorder && _l.type == 'rers') { 175 | this.log ('reordered: ' + this.config.reordered + ' / ' + this.config.reorderPotential); 176 | } 177 | this.log (data.result); 178 | // let a = JSON.parse('{"schedules" : [ { "code": "AURA", "message": "20:50", "destination": "Gare du Nord" }, { "code": "ASAR", "message": "00:49", "destination": "Gare du Nord" }, { "code": "AURA", "message": "20:48", "destination": "Gare du Nord" }]}'); // testing schedule if needed 179 | if (this.config.reorder && _l.type == 'rers' && this.orderResult(data.result)) { 180 | this.log (' schedule reordered in :'); 181 | this.log (data.result); 182 | }; 183 | this.log ('___'); 184 | this.config.infos[_l.id].schedules = data.result.schedules; 185 | this.config.infos[_l.id].lastUpdate = new Date(); 186 | this.loaded = true; 187 | this.sendSocketNotification("DATA", this.config.infos); 188 | }, 189 | 190 | processTraffic: function (data, _l) { 191 | var result, idMaker; 192 | if (this.config.debug) { 193 | console.log('*** processTraffic response receive: ' + (_l.label || '')); 194 | console.log(data.result); //line, title, message 195 | console.log('___'); 196 | } 197 | result = {}; 198 | if (data.result) { 199 | result = data.result; 200 | idMaker = data._metadata.call.split('/'); 201 | } 202 | result.id = idMaker[idMaker.length - 3].toString().toLowerCase() + '/' + idMaker[idMaker.length - 2].toString().toLowerCase() + '/' + idMaker[idMaker.length - 1].toString().toLowerCase(); 203 | result.loaded = true; 204 | this.config.infos[_l.id].status = result; 205 | this.config.infos[_l.id].lastUpdate = new Date(); 206 | this.sendSocketNotification("DATA", this.config.infos); 207 | } 208 | 209 | }); 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-Paris-RATP-PG 2 | 3 | MagicMirror MichMich module to display transportation information for Paris (bus, metro, tramway, RER, autolib & velib) and rain risk in the coming hour for a configured list of stations/ destinations. 4 | 5 | Forked from MMM-HH-LocalTransport see more detailed information on georg90 [blog](https://lane6.de). 6 | 7 | # Presentation 8 | A module to display: 9 | * the different buses, metros, rers & tramways, in order to avoid waiting too much for them when leaving home. 10 | * general traffic information for lines of metros, rers & tramways 11 | * available autolib, utilib, and station spaces, charging slots 12 | * available velib (bike, eBike and dock) 13 | * rain in the coming hour (as per Meteo France) 14 | 15 | # Screenshot -needs to be updated with eBike later-on 16 | ![screenshot](https://github.com/da4throux/MMM-Paris-RATP-PG/blob/master/MMM-Paris-RATP-PG%202.3.png) 17 | 18 | # API 19 | 20 | It is based on the open REST API from Pierre Grimaud https://github.com/pgrimaud/horaires-ratp-api, which does not require any configuration / registration. 21 | It uses a non documented API from Meteo meteofrance for the rain within an hour prediction 22 | It uses the Open Data from Paris City for Autolib, and Velib 23 | 24 | # Install 25 | 26 | 1. Clone repository into `../modules/` inside your MagicMirror folder. 27 | 2. Run `npm install` inside `../modules/MMM-Paris-RATP-PG/` folder 28 | 3. Add the module to the MagicMirror config 29 | ``` 30 | { 31 | module: 'MMM-Paris-RATP-PG', 32 | position: 'bottom_right', 33 | header: 'Connections', 34 | config: { 35 | } 36 | }, 37 | ``` 38 | 39 | # specific configuration 40 | Becareful, configuration changes will only be taken in account once the server side (not only the browser) is restarted. 41 | Three different kind of objects are in the configuration: 42 | * lines: an array that contains an object describing each line to be presented by the modules 43 | * other elements are global to the module 44 | ##lines array 45 | * each line has a type, and each type might have different parameters 46 | ### common to: buses, rers, metros, tramway 47 | * type: mandatory, value in [buses, rers, metros, tramway] 48 | * line: mandatory, typical value: 28 or 'A'... check exact value with: https://api-ratp.pierre-grimaud.fr/v4/lines/buses, https://api-ratp.pierre-grimaud.fr/v4/lines/rers, https://api-ratp.pierre-grimaud.fr/v4/lines/tramways, https://api-ratp.pierre-grimaud.fr/v4/lines/metros 49 | * stations: mandatory: [name of the station] -> found with https://api-ratp.pierre-grimaud.fr/v4/stations/{type}/{line} 50 | * destination: mandatory, either 'A' or 'R' 51 | ### rers only 52 | As destinations do not reveal all the stops for an rer, this allow to filter on code (see https://rera-leblog.fr/les-codes-missions-des-rer-a-dechiffres/) 53 | * mission1: optional, array of letters ['A', 'E'], default is absent = no filtering //keep only rers for which the first letter is present in the array 54 | * mission2: optional, array of letters, default is absent = no filtering //keep only rers for which the second letter is present in the array 55 | ### Traffic 56 | * type: mandatory: traffic 57 | * line: mandatory, based on https://api-ratp.pierre-grimaud.fr/v4/traffic set the line as: [type, line], such as: ['metros', 6], ['rers', 'A']... 58 | * hideTraffic: optional, array of string, if a traffic status belongs to the array, then the traffic is not shown (see the example for usage) 59 | ### Common in Transportation lines 60 | * maximumEntries: optional, int, default = 2, //if the APIs sends several results for the incoming transport how many should be displayed 61 | * converToWaitingTime: optional, boolean, default = true, // messages received from API can be 'hh:mm' in that case convert it in the waiting time 'x mn' 62 | * maxLettersForDestination: optional, int, default = 22, //will limit the length of the destination string 63 | * concatenateArrivals: optional, boolean, default = true, //if for a transport there is the same destination and several times, they will be displayed on one line 64 | ### autolib - I leave it for nostalgia, but no more autolib ... :( 65 | * type: mandatory: autolib 66 | * name: mandatory: public name of the station (check https://opendata.paris.fr/explore/dataset/autolib-disponibilite-temps-reel/ ) 67 | * utilib: optional: boolean: if false: the utilib are aggregated with the bluecar, if true: all three type of cars are detailed 68 | * backup: optional: public name of the station to backup. If that station (set in backup) is empty (no cars - utilib or not), then only this line is displayed. A use case would be: display this station status only if that other station (nearest to me) is empty. The station (set in backup) should be in the lines before (else there might be a delay in displaying the line). 69 | ### velib 70 | * type: mandatory: velib 71 | * stationId: mandatory: digits: please check the station number from the velib application, then you can check if it works out by putting it at the end of the URL: https://opendata.paris.fr/api/records/1.0/search/?dataset=velib-disponibilite-en-temps-reel&refine.station_code= For example: Cassini - Denfer-Rochereau is shown as "14111", and therefore: https://opendata.paris.fr/api/records/1.0/search/?dataset=velib-disponibilite-en-temps-reel&refine.station_code=14111 72 | * keepVelibHistory: optional: boolean: if true, keeps locally in the browser a day of data regarding the station (to be used if velibGraph is set to true later on) 73 | * velibGraph: optional: boolean: shows a graph of velib count for the last day (give an idea of the trend), eBike in blue, total in white 74 | ### Pluie [not working as of sept 2020, needs investigation] 75 | * type: mandatory: pluie 76 | * place: mandatory: integer, example: 751140, take the id from the object returned by: http://www.meteofrance.com/mf3-rpc-portlet/rest/lieu/facet/pluie/search/input=75014 (change 75014 by your postal code) 77 | * pluieAsText: optional, boolean, default = false, // show the weather in the coming hour as text and not icons 78 | * iconSize: optional, example: 0.70, //set the em for the weather icon (each icon is 5 minutes: i.e. there's 12 icons for an hour) 79 | ### common in all lines 80 | * common means: not shared value, but meaningful for all the lines 81 | * label: optional: to rename the object differently if needed 82 | * updateInterval: optional, int, default: 60000, time in ms between pulling request for new times (update request) 83 | * showUpdateAge: optional, boolean, default = true, //add a circled integer such as ①② next to the line name showing the tenths digits of of seconds elapsed since update. 84 | * firstCellColor: optional, color name, // typically first column of the line (superseed the line color): https://dataratp2.opendatasoft.com/explore/dataset/indices-et-couleurs-de-lignes-du-reseau-ferre-ratp/ or wikipedia can give you insights 85 | * lineColor: optional, color name, //set the color of the line 86 | * maxLetters: optional, number, default = 70, will limit the string length for traffic and messages 87 | ## Global element 88 | * debug: false, //console.log more things to help debugging 89 | * reorder: optional, boolean, default = false, //option to reorder the RERs schedule (sometimes they are not sent in coming order, but it seems rare) 90 | ## lineDefault 91 | * lineDefault contains properties that will be common to all lines, but can be superseed at the line level also: so any property from the line, can be set here also, but the following ones, make more sense here also: 92 | * conversion: object of key/ values to convert traffic message or destination. Those message can be very long (and limited through maxLetters also), and it might worth to convert them in a simpler text. by default: 93 | - conversion: {"Trafic normal sur l'ensemble de la ligne." : 'Traffic normal'} 94 | - don't hesitate to add more when there's works on a specific line or others... 95 | * updateInterval: see above 96 | 97 | Config Example: 98 | ```javascript 99 | config: { 100 | debug: false, 101 | lineDefault: { 102 | hideTraffic: [ 103 | "le trafic est interrompu entre Aulnay et Aeroport Charles de Gaulle 2 TGV de 23:00 à fin de service jusqu'au 16/03/18. Bus de remplacement à dispo. (travaux de modernisation)", 104 | "Trafic normal sur l'ensemble de la ligne.", 105 | "le trafic est interrompu entre Nanterre-Prefecture et Cergy/ Poissy de 21:30 à fin de service jusqu'au 16/02/18. Bus de remplacement à dispo. (travaux)", 106 | ], 107 | conversion: { "Trafic normal sur l'ensemble de la ligne." : 'Traffic normal'}, 108 | updateInterval: 1 * 2 * 60 * 1000, 109 | }, 110 | lines: [ 111 | {type: 'buses', line: 38, stations: 'observatoire+++port+royal', destination: 'A', firstCellColor: '#0055c8'}, 112 | {type: 'buses', line: 91, stations: 'observatoire+++port+royal', destination: 'A', firstCellColor: '#dc9600'}, 113 | {type: 'buses', line: 91, stations: 'observatoire+++port+royal', destination: 'R', firstCellColor: '#dc9600', lineColor: 'Brown'}, 114 | {type: 'rers', line: 'B', stations: 'port+royal', destination: 'A', label: 'B', firstCellColor: '#7BA3DC'}, 115 | {type: 'traffic', line: ['rers', 'B'], firstCellColor: 'Blue', lineColor: 'green'}, 116 | {type: 'metros', line: '6', stations: 'raspail', destination: 'A', label: '6', firstCellColor: '#6ECA97'}, 117 | // {type: 'pluie', place: '751140', updateInterval: 1 * 5 * 60 * 1000, label: 'Paris', iconSize: 0.70}, //not working as of sept 2020 118 | // {type: 'autolib', name: 'Paris/Henri%20Barbusse/66', label: 'Barbusse', lineColor: 'green'}, 119 | // {type: 'autolib', name: 'Paris/Michelet/6', label: 'Michelet', utilib: true, backup: 'Paris/Henri%20Barbusse/66'}, 120 | {type: 'velib', stationId: 14111, label: 'Cassini', velibGraph : false, keepVelibHistory: true}, 121 | {type: 'velib', stationId: 6018, label: 'Assas', velibGraph: true, keepVelibHistory: true}, 122 | ], 123 | }, 124 | ``` 125 | # v2.7 126 | -------------------------------------------------------------------------------- /MMM-Paris-RATP-PG.js: -------------------------------------------------------------------------------- 1 | /* Timetable for Paris local transport Module */ 2 | 3 | /* Magic Mirror 4 | * Module: MMM-Paris-RATP-PG 5 | * 6 | * By da4throux 7 | * based on a script from Georg Peters (https://lane6.de) 8 | * and a script from Benjamin Angst http://www.beny.ch 9 | * MIT Licensed. 10 | */ 11 | 12 | Module.register("MMM-Paris-RATP-PG",{ 13 | 14 | // Define module defaults 15 | defaults: { 16 | animationSpeed: 2000, 17 | debug: false, //console.log more things to help debugging 18 | pluie_api: 'http://www.meteofrance.com/mf3-rpc-portlet/rest/pluie/', 19 | ratp_api: 'https://api-ratp.pierre-grimaud.fr/v4/', 20 | autolib_api: 'https://opendata.paris.fr/api/records/1.0/search/?dataset=autolib-disponibilite-temps-reel&refine.public_name=', 21 | velib_api: 'https://opendata.paris.fr/api/records/1.0/search/?dataset=velib-disponibilite-en-temps-reel&refine.stationcode=', 22 | velib_api_max: 5000, //nb of request max par jour 23 | conversion: { "Trafic normal sur l'ensemble de la ligne." : 'Traffic OK'}, 24 | reorder: false, //no reorder of rers schedule (seems to be quite rare) 25 | reordered: 0, 26 | reorderPotential: 0, 27 | pluieIconConverter: { 28 | "Pas de précipitations" : 'wi-day-cloudy', 29 | "Précipitations faibles": 'wi-day-showers', 30 | "Précipitations modérés": 'wi-day-rain', 31 | "Précipidations fortes": 'wi-day-storm-showers', 32 | }, 33 | pluieIconColors: { 34 | "Pas de précipitations" : 'blue', 35 | "Précipitations faibles": 'yellow', 36 | "Précipitations modérés": 'orange', 37 | "Précipidations fortes": 'red', 38 | }, 39 | autolibIconConverter: { 40 | "cars" : 'car', 41 | "parking" : 'map-marker', 42 | "utilib" : 'wrench', 43 | "utilib-0.9" : 'cube', 44 | "utilib-1.4" : 'cubes', 45 | "charge" : 'bolt', 46 | "closed" : 'window-close', 47 | }, 48 | line_template: { 49 | updateInterval: 1 * 60 * 1000, 50 | maximumEntries: 2, //if the APIs sends several results for the incoming transport how many should be displayed 51 | maxLettersForDestination: 22, //will limit the length of the destination string 52 | maxLetters: 70, //will limit the length of other second column messages 53 | convertToWaitingTime: true, // messages received from API can be 'hh:mm' in that case convert it in the waiting time 'x mn' 54 | concatenateArrivals: true, //if for a transport there is the same destination and several times, they will be displayed on one line 55 | initialLoadDelay: 0, // start delay seconds 56 | showUpdateAge: true, 57 | pluieAsText: false, 58 | velibTrendDay : true, 59 | conversion: {}, 60 | hideTraffic: [], 61 | }, 62 | updateDomFrequence: 10000, 63 | }, 64 | 65 | // Define required scripts. 66 | getStyles: function() { 67 | return ["MMM-Paris-RATP-Transport.css", "font-awesome.css", "weather-icons.css"]; 68 | }, 69 | 70 | cleanStoreVelibHistory: function(_l, _first) { 71 | var now = new Date(); 72 | var j, velib, evelib, dock, maxVelibArchiveAge, velibArchiveCleaned, oldHistory; 73 | if (_first) { 74 | //récupération de l'historique si existant et cleaning 75 | _l.velibHistory = localStorage['velib-' + _l.stationId] ? JSON.parse(localStorage['velib-' + _l.stationId]) : []; 76 | } 77 | velibArchiveCleaned = 0; 78 | maxVelibArchiveAge = _l.velibTrendDay ? 24 * 60 * 60 : _l.velibTrendTimeScale || 60 * 60; 79 | oldHistory = _l.velibHistory; 80 | //remove old lines, but keep at least one out of frame if any 81 | while ((oldHistory.length > 1) && ((((now - new Date(oldHistory[1].lastUpdate)) / 1000) > maxVelibArchiveAge) || !oldHistory[1].lastUpdate) ) { 82 | oldHistory.shift(); 83 | } 84 | _l.velibHistory = []; 85 | if (oldHistory.length > 0 && oldHistory[0].data) { 86 | oldHistory[0].data.update = oldHistory[0].data.update ? oldHistory[0].data.update : oldHistory[0].lastUpdate; 87 | _l.velibHistory.push(oldHistory[0]); 88 | velib = oldHistory[0].data.nbbike; 89 | evelib = oldHistory[0].data.nbebike; 90 | dock = oldHistory[0].data.nbfreeedock; 91 | for (j = 1; j < oldHistory.length; j++) { 92 | if (velib !== oldHistory[j].data.nbbike || oldHistory[j].data.nbfreeedock !== dock || evelib != oldHistory[j].data.nbebike) { 93 | oldHistory[j].data.update = oldHistory[j].data.update ? oldHistory[j].data.update : oldHistory[j].lastUpdate; 94 | velib = oldHistory[j].data.nbbike 95 | evelib = oldHistory[j].data.nbebike; 96 | dock = oldHistory[j].data.nbfreeedock; 97 | _l.velibHistory.push(oldHistory[j]); 98 | } else { 99 | _l.velibHistory[_l.velibHistory.length - 1].lastUpdate = oldHistory[j].lastUpdate; 100 | } 101 | } 102 | } 103 | localStorage['velib-' + _l.stationId] = JSON.stringify(_l.velibHistory); 104 | if (this.config.debug && _first) { 105 | console.log ('First load size of velib History for ' + _l.stationId + ' is: ' + _l.velibHistory.length); 106 | console.log (velibArchiveCleaned + ' elements removed'); 107 | console.log (_l.velibHistory); 108 | } 109 | return true; 110 | }, 111 | 112 | // Define start sequence. 113 | start: function() { 114 | var l, i, nb_velib = 0, velibs = []; 115 | Log.info("Starting module: " + this.name); 116 | this.config.infos = []; 117 | this.traffic = []; 118 | if (!this.config.lines) { 119 | this.config.lines = this.config.busStations || []; //v1 legacy support for migration 120 | } 121 | for (i=0; i < this.config.lines.length; i++) { 122 | this.config.infos[i]={}; 123 | l = Object.assign(JSON.parse(JSON.stringify(this.config.line_template)), 124 | JSON.parse(JSON.stringify(this.config.lineDefault || {})), 125 | JSON.parse(JSON.stringify(this.config.lines[i]))); 126 | l.id = i; 127 | switch (l.type) { 128 | case 'tramways': 129 | case 'bus': 130 | case 'buses': 131 | case 'rers': 132 | case 'metros': 133 | if (l.type == 'bus') { l.type = 'buses';} //to avoid update config from v3 to v4 134 | l.url = this.config.ratp_api + 'schedules/' + l.type + '/' + l.line.toString().toLowerCase() + '/' + l.stations + '/' + l.destination; // get schedule for that bus 135 | break; 136 | case 'traffic': 137 | l.url = this.config.ratp_api + 'traffic/' +l.line[0] + '/' + l.line[1]; 138 | break; 139 | case 'pluie': 140 | l.url = this.config.pluie_api + l.place; 141 | break; 142 | case 'autolib': 143 | l.url = this.config.autolib_api + l.name; 144 | break; 145 | case 'velib': 146 | l.url = this.config.velib_api + l.stationId; 147 | nb_velib++; 148 | velibs.push(l); 149 | if (l.velibGraph || l.keepVelibHistory) { 150 | this.cleanStoreVelibHistory(l, true); 151 | } 152 | break; 153 | default: 154 | if (this.config.debug) { console.log('Unknown request type: ' + l.type)} 155 | } 156 | this.config.lines[i] = l; 157 | } 158 | if (nb_velib > 0) { 159 | for (i = 0; i < velibs.length; i++) { 160 | velibs[i].updateInterval = Math.max(Math.ceil(24 * 60 * 60 / this.config.velib_api_max * nb_velib) * 1000, velibs[i].updateInterval); 161 | } 162 | console.log ('MMM RATP: setting velib update Interval to: ' + Math.ceil( 24 * 60 * 60 / this.config.velib_api_max * nb_velib) + 's'); 163 | } 164 | this.sendSocketNotification('SET_CONFIG', this.config); 165 | this.loaded = false; 166 | var self = this; 167 | setInterval(function () { 168 | self.caller = 'updateInterval'; 169 | self.updateDom(); 170 | }, this.config.updateDomFrequence); 171 | }, 172 | 173 | getHeader: function () { 174 | var header = this.data.header; 175 | return header; 176 | }, 177 | 178 | buildVelibGraph: function(l, d) { 179 | var dataIndex, dataTimeStamp, now = new Date(); 180 | var rowTrend = document.createElement("tr"); 181 | var cellTrend = document.createElement("td"); 182 | var trendGraph = document.createElement('canvas'); 183 | trendGraph.className = "velibTrendGraph"; 184 | trendGraph.width = l.velibTrendWidth || 400; 185 | trendGraph.height = l.velibTrendHeight || 100; 186 | trendGraph.timeScale = l.velibTrendDay ? 24 * 60 * 60 : l.velibTrendTimeScale || 60 * 60; // in nb of seconds, the previous hour 187 | l.velibTrendZoom = l.velibTrendZoom || 30 * 60; //default zoom windows is 30 minutes for velibTrendDay 188 | var ctx = trendGraph.getContext('2d'); 189 | var currentStation = l.stationId; 190 | var previousX = trendGraph.width; 191 | var inTime = false; 192 | for (dataIndex = l.velibHistory.length - 1; dataIndex >= 0 ; dataIndex--) { //start from most recent 193 | dataTimeStamp = (now - new Date(l.velibHistory[dataIndex].data.update)) / 1000; // time of the event in seconds ago 194 | if (dataTimeStamp < trendGraph.timeScale || inTime) { 195 | inTime = dataTimeStamp < trendGraph.timeScale; // compute the last one outside of the time window 196 | if (dataTimeStamp - trendGraph.timeScale < 10 * 60) { //takes it only if it is within 10 minutes of the closing windows 197 | dataTimeStamp = Math.min(dataTimeStamp, trendGraph.timeScale); //to be sure it does not exit the graph 198 | var x, y, ye; 199 | if (l.velibTrendDay) { 200 | if ( dataTimeStamp < l.velibTrendZoom ) { //1st third in zoom mode 201 | x = (1 - dataTimeStamp / l.velibTrendZoom / 3) * trendGraph.width; 202 | } else if (dataTimeStamp < trendGraph.timeScale - l.velibTrendZoom) { //middle in compressed mode 203 | x = (2/3 - (dataTimeStamp - l.velibTrendZoom) / (trendGraph.timeScale - 2 * l.velibTrendZoom)/ 3) * trendGraph.width; 204 | } else { 205 | x = (1 / 3 - (dataTimeStamp - trendGraph.timeScale + l.velibTrendZoom)/ l.velibTrendZoom / 3) * trendGraph.width; 206 | } 207 | } else { 208 | x = (1 - dataTimeStamp / trendGraph.timeScale) * trendGraph.width; 209 | } 210 | y = (l.velibHistory[dataIndex].data['nbbike'] + l.velibHistory[dataIndex].data['nbebike']) / l.velibHistory[dataIndex].data['nbedock'] * trendGraph.height * 4 / 5; 211 | ye = (l.velibHistory[dataIndex].data['nbebike']) / l.velibHistory[dataIndex].data['nbedock'] * trendGraph.height * 4 / 5; 212 | ctx.fillStyle = 'white'; 213 | ctx.fillRect(x, trendGraph.height - y - 1, previousX - x, Math.max(y, 1)); //a thin line even if it's zero 214 | ctx.fillStyle = 'blue'; 215 | ctx.fillRect(x, trendGraph.height - ye - 1, previousX - x, Math.max(ye, 1)); //electric bike graph 216 | previousX = x; 217 | } 218 | } 219 | } 220 | // var bodyStyle = window.getComputedStyle(document.getElementsByTagName('body')[0], null); 221 | // ctx.font = bodyStyle.getPropertyValue(('font-size')) + ' ' + ctx.font.split(' ').slice(-1)[0]; //00px sans-serif 222 | ctx.font = Math.round(trendGraph.height / 5) + 'px ' + ctx.font.split(' ').slice(-1)[0]; 223 | ctx.fillStyle = 'grey'; 224 | ctx.textAlign = 'center'; 225 | ctx.fillText(l.label || l.name, trendGraph.width / 2, Math.round(trendGraph.height / 5)); 226 | ctx.textAlign = 'left'; 227 | ctx.fillText(d.data['nbbike'] + d.data['nbebike'], 10, trendGraph.height - 10); 228 | ctx.fillText(d.data['nbedock'], 10, Math.round(trendGraph.height / 5) + 10); 229 | if (l.velibTrendDay) { 230 | ctx.font = Math.round(trendGraph.height / 10) + 'px ' + ctx.font.split(' ').slice(-1)[0]; 231 | ctx.fillText(Math.round(l.velibTrendZoom / 60) + 'mn', trendGraph.width * 5 / 6, trendGraph.height / 2); 232 | ctx.fillText(Math.round(l.velibTrendZoom / 60) + 'mn', trendGraph.width / 6, trendGraph.height / 2); 233 | ctx.strokeStyle = 'grey'; 234 | ctx.setLineDash([5, 15]); 235 | ctx.beginPath(); 236 | ctx.moveTo(2/3 * trendGraph.width, 0); 237 | ctx.lineTo(2/3 * trendGraph.width, 100); 238 | ctx.stroke(); 239 | ctx.moveTo(trendGraph.width / 3, 0); 240 | ctx.lineTo(trendGraph.width / 3, 100); 241 | ctx.stroke(); 242 | var hourMark = new Date(); var alpha; 243 | hourMark.setMinutes(0); hourMark.setSeconds(0); 244 | alpha = (hourMark - now + 24 * 60 * 60 * 1000 - l.velibTrendZoom * 1000) / (24 * 60 * 60 * 1000 - 2 * l.velibTrendZoom * 1000); 245 | alpha = (hourMark - now + l.velibTrendZoom * 1000) / (24 * 60 * 60 * 1000) * trendGraph.width; 246 | for (var h = 0; h < 24; h = h + 2) { 247 | ctx.fillStyle = 'red'; 248 | ctx.textAlign = 'center'; 249 | ctx.font = Math.round(trendGraph.height / 12) + 'px'; 250 | ctx.fillText((hourMark.getHours() + 24 - h) % 24, (2 - h / 24) * trendGraph.width / 3 + alpha, h % 12 * trendGraph.height / 12 / 3 + trendGraph.height / 3); 251 | } 252 | } 253 | cellTrend.colSpan = '3'; //so that it takes the whole row 254 | cellTrend.appendChild(trendGraph); 255 | rowTrend.appendChild(cellTrend); 256 | return (rowTrend); 257 | }, 258 | 259 | // Override dom generator. 260 | getDom: function() { 261 | var now = new Date(); 262 | var wrapper = document.createElement("div"); 263 | var lines = this.config.lines; 264 | var i, j, l, d, n, firstLine, delta, lineColor, cars, currentHistory; 265 | var table = document.createElement("table"); 266 | var stopIndex, firstCell, secondCell; 267 | var previousRow, previousDestination, previousMessage, row, comingBus, iconSize, nexts; 268 | if (lines.length > 0) { 269 | if (!this.loaded) { 270 | wrapper.innerHTML = "Loading connections ..."; 271 | wrapper.className = "dimmed light small"; 272 | return wrapper; 273 | } else { 274 | wrapper.className = "paristransport"; 275 | wrapper.appendChild(table); 276 | table.className = "small"; 277 | } 278 | } else { 279 | wrapper.className = "small"; 280 | wrapper.innerHTML = "Configuration now requires a 'lines' element.
Check github da4throux/MMM-Paris-RATP-PG
for more information"; 281 | } 282 | if (this.config.busStations) { 283 | row = document.createElement("tr"); 284 | firstCell = document.createElement("td"); 285 | firstCell.innerHTML = "Configuration now requires to rename your 'busStations' element in 'lines'.
Check github da4throux/MMM-Paris-RATP-PG
for more information"; 286 | firstCell.className = "dimmed light small"; 287 | firstCell.colSpan = 3; 288 | row.appendChild(firstCell); 289 | table.appendChild(row); 290 | } 291 | for (i = 0; i < lines.length; i++) { 292 | l = lines[i]; // line config 293 | d = this.infos[i]; // data received for the line 294 | firstLine = true; 295 | firstCellHeader = ''; 296 | if ((new Date() - Date.parse(d.lastUpdate) )/ 1000 > 0 && l.showUpdateAge) { 297 | delta = Math.floor((new Date() - Date.parse(d.lastUpdate) )/ 1000 / 10); 298 | if (delta <= 20) { 299 | firstCellHeader += '&#' + (9312 + delta) + ';'; 300 | } else if (delta > 20) { 301 | firstCellHeader += '⓿'; 302 | } 303 | } 304 | lineColor = l.lineColor ? 'color:' + l.lineColor + ' !important' : false; 305 | switch (l.type) { 306 | case "traffic": 307 | row = document.createElement("tr"); 308 | row.id = 'line-' + i; 309 | firstCell = document.createElement("td"); 310 | firstCell.className = "align-right bright"; 311 | firstCell.innerHTML = firstCellHeader + (l.label || l.line[1]); 312 | if (lineColor) { 313 | firstCell.setAttribute('style', lineColor); 314 | } 315 | if (l.firstCellColor) { 316 | firstCell.setAttribute('style', 'color:' + l.firstCellColor + ' !important'); 317 | } 318 | row.appendChild(firstCell); 319 | secondCell = document.createElement("td"); 320 | secondCell.className = "align-left"; 321 | if (d.status && l.hideTraffic.indexOf(d.status.message) < 0 && this.traffic.indexOf(d.status.message) < 0) { 322 | this.traffic.push(d.status.message); 323 | if (this.config.debug) { console.warn(this.traffic); } //to find it more easily 324 | } 325 | secondCell.innerHTML = d.status ? l.conversion[d.status.message] || d.status.message.substr(0, l.maxLetters) : 'N/A'; 326 | secondCell.colSpan = 2; 327 | if (lineColor) { 328 | secondCell.setAttribute('style', lineColor); 329 | } 330 | row.appendChild(secondCell); 331 | if (l.hideTraffic.indexOf(d.status.message) < 0) { 332 | table.appendChild(row); 333 | } 334 | break; 335 | case "buses": 336 | case "metros": 337 | case "tramways": 338 | case "rers": 339 | nexts = d.schedules || [{message: 'N/A', destination: 'N/A'}]; 340 | let currentEntries = 0; 341 | for (var rank = 0; (currentEntries < l.maximumEntries) && (rank < nexts.length); rank++) { 342 | let showEntry = true; 343 | n = nexts[rank]; //next transport 344 | if (l.type == 'rers' && l.mission1 && l.mission1.indexOf(n.code[0]) < 0) { 345 | showEntry = false; 346 | } 347 | if (l.type == 'rers' && l.mission2 && l.mission2.indexOf(n.code[1]) < 0) { 348 | showEntry = false; 349 | } 350 | if (showEntry) { 351 | currentEntries++; 352 | row = document.createElement("tr"); 353 | row.id = 'line-' + i + '-' + 'rank'; 354 | var firstCell = document.createElement("td"); 355 | firstCell.className = "align-right bright"; 356 | firstCell.innerHTML = firstLine ? firstCellHeader + (l.label || l.line) : ' '; 357 | if (lineColor) { 358 | firstCell.setAttribute('style', lineColor); 359 | } 360 | if (l.firstCellColor) { 361 | firstCell.setAttribute('style', 'color:' + l.firstCellColor + ' !important'); 362 | } 363 | row.appendChild(firstCell); 364 | var destinationCell = document.createElement("td"); 365 | destinationCell.innerHTML = l.conversion[n.destination] || n.destination.substr(0, l.maxLettersForDestination); 366 | destinationCell.className = "align-left"; 367 | if (lineColor) { 368 | destinationCell.setAttribute('style', lineColor); 369 | } 370 | row.appendChild(destinationCell); 371 | var depCell = document.createElement("td"); 372 | depCell.className = "bright"; 373 | if (l.convertToWaitingTime && /^\d{1,2}[:][0-5][0-9]$/.test(n.message)) { 374 | var transportTime = n.message.split(':'); 375 | var trainDate = new Date(0, 0, 0, transportTime[0], transportTime[1]); 376 | var startDate = new Date(0, 0, 0, now.getHours(), now.getMinutes(), now.getSeconds()); 377 | var waitingTime = trainDate - startDate; 378 | if (startDate > trainDate ) { 379 | if (startDate - trainDate < 1000 * 60 * 2) { 380 | waitingTime = 0; 381 | } else { 382 | waitingTime += 1000 * 60 * 60 * 24; 383 | } 384 | } 385 | waitingTime = Math.floor(waitingTime / 1000 / 60); 386 | depCell.innerHTML = waitingTime + ' mn'; 387 | } else { 388 | depCell.innerHTML = l.conversion[n.message] || n.message.substr(0, l.maxLetters); 389 | } 390 | if (lineColor) { 391 | depCell.setAttribute('style', lineColor); 392 | } 393 | row.appendChild(depCell); 394 | if (l.concatenateArrivals && !firstLine && (n.destination == previousDestination)) { 395 | previousMessage += ' / ' + depCell.innerHTML; 396 | previousRow.getElementsByTagName('td')[2].innerHTML = previousMessage; 397 | } else { 398 | table.appendChild(row); 399 | previousRow = row; 400 | previousMessage = depCell.innerHTML; 401 | previousDestination = n.destination; 402 | } 403 | firstLine = false; 404 | } 405 | } 406 | break; 407 | case "pluie": 408 | row = document.createElement("tr"); 409 | row.id = 'line-' + i; 410 | firstCell = document.createElement("td"); 411 | firstCell.className = "align-right bright"; 412 | firstCell.innerHTML = firstCellHeader + (l.label || l.place); 413 | if (lineColor) { 414 | firstCell.setAttribute('style', lineColor); 415 | } 416 | if (l.firstCellColor) { 417 | firstCell.setAttribute('style', 'color:' + l.firstCellColor + ' !important'); 418 | } 419 | row.appendChild(firstCell); 420 | secondCell = document.createElement("td"); 421 | secondCell.colSpan = 2; 422 | if (lineColor) { 423 | secondCell.setAttribute('style', lineColor); 424 | } 425 | if (l.pluieAsText) { 426 | secondCell.className = "align-left"; 427 | secondCell.innerHTML = d.niveauPluieText.join('
'); 428 | } else { 429 | secondCell.className = "align-center"; 430 | secondCell.innerHTML = ''; 431 | iconSize = l.iconSize ? "font-size: " + l.iconSize + "em" : ""; 432 | for (j = 0; j < d.dataCadran.length; j++) { 433 | var iconColor = ''; 434 | iconColor = l.pluieNoColor ? '' : 'color:' + this.config.pluieIconColors[d.dataCadran[j].niveauPluieText] + ' !important;'; 435 | secondCell.innerHTML += ''; 436 | } 437 | } 438 | row.appendChild(secondCell); 439 | table.appendChild(row); 440 | break; 441 | case "velib": 442 | if (l.keepVelibHistory || l.velibGraph) { 443 | l.velibHistory.push(d); 444 | this.cleanStoreVelibHistory(l); 445 | } 446 | row = document.createElement("tr"); 447 | row.id = 'line-' + i; 448 | firstCell = document.createElement("td"); 449 | firstCell.className = "align-right bright"; 450 | firstCell.innerHTML = firstCellHeader + (l.label || l.stationId); 451 | if (lineColor) { 452 | firstCell.setAttribute('style', lineColor); 453 | } 454 | if (l.firstCellColor) { 455 | firstCell.setAttribute('style', 'color:' + l.firstCellColor + ' !important'); 456 | } 457 | row.appendChild(firstCell); 458 | secondCell = document.createElement("td"); 459 | secondCell.colSpan = 2; 460 | if (lineColor) { 461 | secondCell.setAttribute('style', lineColor); 462 | } 463 | secondCell.style.align = "center"; 464 | if (d.data && d.data.station_state == 'Operative') { //&&& 465 | secondCell.innerHTML = d.data['nbbike'] + ' '; 466 | secondCell.innerHTML += d.data['nbebike'] + ' '; 467 | secondCell.innerHTML += d.data['nbfreeedock'] + ' '; 468 | } else { 469 | secondCell.innerHTML = ' '; 470 | } 471 | row.appendChild(secondCell); 472 | table.appendChild(row); 473 | if (l.velibGraph) { 474 | table.appendChild(this.buildVelibGraph(l, d)); 475 | } 476 | break; 477 | case "autolib": 478 | row = document.createElement("tr"); 479 | row.id = 'line-' + i; 480 | firstCell = document.createElement("td"); 481 | firstCell.className = "align-right bright"; 482 | firstCell.innerHTML = firstCellHeader + (l.label || l.place); 483 | if (lineColor) { 484 | firstCell.setAttribute('style', lineColor); 485 | } 486 | if (l.firstCellColor) { 487 | firstCell.setAttribute('style', 'color:' + l.firstCellColor + ' !important'); 488 | } 489 | row.appendChild(firstCell); 490 | secondCell = document.createElement("td"); 491 | secondCell.colSpan = 2; 492 | if (lineColor) { 493 | secondCell.setAttribute('style', lineColor); 494 | } 495 | autolib = d.data['cars_counter_bluecar']; 496 | cars = autolib + d.data['cars_counter_utilib_1.4'] + d.data['cars_counter_utilib']; 497 | l.empty = cars < 1; 498 | //secondCell.className = "aligncenter"; 499 | secondCell.style.align = "center"; 500 | secondCell.innerHTML = (l.utilib ? autolib : cars) 501 | + ' '; 502 | if (l.utilib) { 503 | secondCell.innerHTML += 504 | d.data['cars_counter_utilib_1.4'] 505 | + ' ' 506 | + d.data['cars_counter_utilib'] 507 | + ' '; 508 | } 509 | secondCell.innerHTML += 510 | d.data.slots 511 | + ' ' 512 | + d.data['charge_slots'] 513 | + ''; 514 | if (d.data.status === 'closed') { 515 | secondCell.innerHTML = ''; 516 | } 517 | row.appendChild(secondCell); 518 | if (l.backup) { 519 | for (j = 0; j < lines.length; j++) { 520 | if ((lines[j].name === l.backup) && lines[j].empty) { 521 | table.appendChild(row); 522 | break; 523 | } 524 | } 525 | } else { 526 | table.appendChild(row); 527 | } 528 | break; 529 | default: 530 | if (this.config.debug) { console.log('Unknown request type: ' + l.type)} 531 | } 532 | } 533 | return wrapper; 534 | }, 535 | 536 | socketNotificationReceived: function(notification, payload) { 537 | var now = new Date(); 538 | this.caller = notification; 539 | switch (notification) { 540 | case "DATA": 541 | this.infos = payload; 542 | this.loaded = true; 543 | break; 544 | } 545 | } 546 | }); 547 | --------------------------------------------------------------------------------