├── .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 | 
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 |
--------------------------------------------------------------------------------