· 4 months ago · May 18, 2025, 10:25 PM
1/* global Log Module */
2
3/* MagicMirror²
4 * Module: MMM-DBF
5 *
6 * By Marc Helpenstein <helpi9007@gmail.com>
7 * MIT Licensed.
8 */
9
10Module.register("MMM-DBF", {
11 defaults: {
12 updateInterval: 60000, // 1 minute
13 retryDelay: 30000, // 30 seconds
14 station: "Düsseldorf Hbf",
15 platform: "",
16 via: "",
17 showApp: false,
18 showArrivalTime: false,
19 showRealTime: false,
20 onlyArrivalTime: false,
21 onlyDepartureTime: true,
22 numberOfResults: 10,
23 hideLowDelay: false,
24 withoutDestination: "",
25 onlyDestination: "",
26 train: "",
27 height: "600px",
28 width: "400px",
29 setTableWidth: "",
30 timeOption: "time", // time+countdown or countdown
31 showDelayMsg: false,
32 },
33
34 requiresVersion: "2.1.0",
35
36 /**
37 * @description Helper function to generate API url
38 *
39 * @returns {String} url
40 */
41 generateUrl() {
42 this.sendNotification("GENERATING_URL", { message: '[MMM-DBF] Generating URL from dbf.finalrewind.org ...'});
43 let baseUrl = "https://dbf.finalrewind.org/";
44 baseUrl += `${this.config.station}?&hide_opts=1`
45 // All optional parameters follow here
46 if (this.config.platform) {
47 baseUrl += `&platforms=${this.config.platform}`;
48 }
49 if (this.config.via) {
50 baseUrl += `&via=${this.config.via}`;
51 }
52 if (this.config.showArrivalTime) {
53 baseUrl += "&detailed=1";
54 }
55 if (this.config.showRealTime) {
56 baseUrl += "&show_realtime=1";
57 }
58 if (this.config.onlyArrivalTime) {
59 baseUrl += "&admode=arr";
60 }
61 else if (this.config.onlyDepartureTime) {
62 baseUrl += "&admode=dep";
63 }
64 if (this.config.hideLowDelay) {
65 baseUrl += "&hidelowdelay=1";
66 }
67 return baseUrl;
68 },
69
70 /**
71 * @description Calls updateIterval
72 */
73 start() {
74 // Flag for check if module is loaded
75 this.loaded = false;
76 // Schedule update timer.
77 this.getData();
78 // Show that the module is running by issuing a notification
79 this.sendNotification("MODULE_LOADED", { message: '[MMM-DBF] Module loaded.'});
80 },
81
82 /**
83 * @description Gets data from dbf.finalrewind.org
84 */
85 async getData() {
86 const self = this;
87 const urlApi = `${this.generateUrl()}&mode=json&version=3`;
88 const dataRequest = await fetch(urlApi);
89
90 if (!dataRequest.ok) {
91 // Show that the data request has an error
92 this.sendNotification("ERROR_DATA_REQUEST_NOT_OK", { message: '[MMM-DBF] Data request is not okay.'});
93 let message = `An error has occurred: ${dataRequest.status}`;
94 if (dataRequest.status === 300) {
95 message += " - Ambiguous station name.";
96 }
97 throw new Error(message);
98 }
99 else {
100 const data = await dataRequest.json();
101 // Show that the data request is being processed
102 this.sendNotification("START_PROCESSING_DATA", { message: '[MMM-DBF] Data processing started.'});
103 self.processData(data);
104 }
105 self.scheduleUpdate(self.config.retryDelay);
106 },
107
108 /**
109 * @description Schedule next update.
110 * @param {int} delay - Milliseconds before next update.
111 */
112 scheduleUpdate(delay) {
113 const self = this;
114 let nextLoad = this.config.updateInterval;
115 if (typeof delay !== "undefined" && delay >= 0) {
116 nextLoad = delay;
117 }
118 setTimeout(() => {
119 self.getData();
120 }, nextLoad);
121
122 if (!this.config.showApp) {
123 // Show that the module is updating its content
124 this.sendNotification("UPDATING_DOM", { message: '[MMM-DBF] DOM is being updated.'});
125 this.updateDom();
126 }
127 },
128
129 /**
130 * @description Create App Frame or HTML table
131 *
132 * @returns {HTMLIframeElement}
133 */
134 getDom() {
135 // Show that the module is updating its content
136 this.sendNotification("CREATING_DOM", { message: '[MMM-DBF] DOM is being created.'});
137 if (this.config.showApp) {
138 // Show that the module is updating its content
139 this.sendNotification("CREATING_IFRAME", { message: '[MMM-DBF] DOM is being created.'});
140 const iframe = document.createElement("IFRAME");
141 iframe.style = "border:0";
142 iframe.width = this.config.width;
143 iframe.height = this.config.height;
144 iframe.src = this.generateUrl();
145 // Show that the module has set up the iFrame
146 this.sendNotification("IFRAME_AND_URL_GENERATED", { message: '[MMM-DBF] URL for iFrame successfully generated.'});
147 return iframe;
148 }
149 // Show that the module is updating its content
150 this.sendNotification("CREATING_TABLE", { message: '[MMM-DBF] Table in DOM is being created.'});
151 const tableWrapper = document.createElement("table");
152 this.sendNotification("EMPTY_TABLE_CREATED", { message: '[MMM-DBF] Empty table in DOM successfully created.'});
153 tableWrapper.className = "small mmm-dbf-table";
154 this.sendNotification("CLASS_NAME_DEFINED", { message: '[MMM-DBF] Class name "small mmm-dbf-table" successfully defined.'});
155 if (this.dataRequest) {
156 // The data request seems valid
157 this.sendNotification("DATA_REQUEST_AVAILABLE", { message: '[MMM-DBF] The data request is available!'});
158 if (!this.dataRequest.error) {
159 // The data request does not have an error
160 this.sendNotification("DATA_REQUEST_VALID", { message: '[MMM-DBF] The data request seems valid!'});
161 if (this.config.setTableWidth) {
162 // Set custom table width
163 this.sendNotification("SET_CUSTOM_TABLE_WIDTH", { message: '[MMM-DBF] A custom table width shall be set!'});
164 tableWrapper.style.width = this.config.setTableWidth;
165 this.sendNotification("CUSTOM_TABLE_WIDTH_SET", { message: '[MMM-DBF] A custom table width was set!'});
166 }
167 const { departures } = this.dataRequest;
168 this.sendNotification("DEPARTURES_READ", { message: '[MMM-DBF] Departures were successfully read out!'});
169 const tableHead = this.createTableHeader(departures);
170 tableWrapper.appendChild(tableHead);
171 this.createTableContent(departures, tableWrapper);
172 // Table content successfully generated
173 this.sendNotification("TABLE_CONTENT_GENERATED", { message: '[MMM-DBF] Table content successfully generated.'});
174 }
175 else {
176 // An error occurred which is being logged
177 this.sendNotification("DATA_REQUEST_ERROR_LOGGED", { message: '[MMM-DBF] Error while performing data request was logged.'});
178 Log.error(this.dataRequest.error);
179 }
180 }
181 else {
182 // The data request is empty somehow
183 this.sendNotification("DATA_REQUEST_EMPTY", { message: '[MMM-DBF] The data request is empty!'});
184 }
185 return tableWrapper;
186 },
187
188 /**
189 * @description Get the size for showing entrys
190 * @param {Object[]} departures
191 */
192 getSize(departures) {
193 if (departures.length < this.config.numberOfResults) {
194 return departures.length;
195 }
196 return this.config.numberOfResults;
197 },
198
199 /**
200 * @description Check delay exist
201 * @param {Object[]} departures
202 */
203 checkDelayExist: function (departures) {
204 for (let index = 0; index < this.getSize(departures); index++) {
205 if (departures[index].delayDeparture) {
206 if (this.config.hideLowDelay && departures[index].delayDeparture >= 5) {
207 return true;
208 }
209 if (!this.config.hideLowDelay) {
210 return true;
211 }
212 }
213 }
214 return false;
215 },
216
217 /**
218 * @description Get col number
219 */
220 getColDelay() {
221 if (this.config.via !== "") {
222 return 5;
223 }
224 return 4;
225 },
226
227 /**
228 * @param {Object} train
229 */
230 getViaFromRoute(train) {
231 const viaConfigList = this.config.via.split(",");
232 const route = train.via;
233 for (let i = 0; i < route.length; i += 1) {
234 const city = route[i];
235 for (let j = 0; j < viaConfigList.length; j += 1) {
236 if (city.includes(viaConfigList[j])) {
237 return viaConfigList[j];
238 }
239 }
240 }
241 return false;
242 },
243
244 /**
245 * @description Check if destination is in list config.withoutDestination
246 * @param {Object} train
247 */
248 checkDestination(train, destinationConfig) {
249 const destinations = destinationConfig.split(",");
250 for (let index = 0; index < destinations.length; index += 1) {
251 if (train.destination === destinations[index]) {
252 return true;
253 }
254 }
255 return false;
256 },
257
258 /**
259 * @description Check if train is in list config.train
260 * @param {Object} train
261 */
262 checkTrain(train) {
263 const trains = this.config.train.split(",");
264 const trainName = train.train.split(" ")[0] + train.train.split(" ")[1];
265 for (let i = 0; i < trains.length; i += 1) {
266 if (trainName.includes(trains[i])) {
267 return true;
268 }
269 }
270 return false;
271 },
272
273 /**
274 * @description Checks time and return day/hour/mins
275 * @param {int} time - Remaining time
276 */
277 convertTime(scheduledTime) {
278 const time = this.calculateTime(scheduledTime);
279 if (time >= 3600) {
280 const strTime = Math.floor(time / 3600).toString();
281 return `+${strTime} ${this.translate("HOUR")}`;
282 }
283 if (time >= 60) {
284 const strTime = Math.floor(time / 60).toString();
285 return `${strTime} ${this.translate("MINUTE")}`;
286 }
287 return this.translate("NOW");
288 },
289
290 /**
291 * @description Calculate remaining time
292 * @param {int:int} scheduledTime
293 */
294 calculateTime(scheduledTime) {
295 const d = new Date();
296 const time = scheduledTime.split(":");
297 const dateTrain = new Date(
298 d.getFullYear(),
299 d.getMonth(),
300 d.getDate(),
301 time[0],
302 time[1],
303 );
304 const newStamp = new Date().getTime();
305 return Math.round((dateTrain.getTime() - newStamp) / 1000);
306 },
307
308 /**
309 * @description Check msg exists
310 * @param {Object[]} departures
311 */
312 checkMsgExist(departures) {
313 for (let index = 0; index < this.getSize(departures); index += 1) {
314 if (
315 departures[index] !== undefined
316 && departures[index].messages.delay.length > 0
317 ) {
318 return true;
319 }
320 }
321 return false;
322 },
323
324 /**
325 * @description Creates the header for the Table
326 */
327 createTableHeader(departures) {
328 const tableHead = document.createElement("tr");
329 tableHead.className = "border-bottom";
330
331 const tableHeadValues = [
332 this.translate("TRAIN"),
333 this.translate("TRACK"),
334 this.translate("DESTINATION"),
335 ];
336
337 if (this.config.via !== "") {
338 tableHeadValues.push(this.translate("VIA"));
339 }
340 if (!this.config.onlyArrivalTime) {
341 tableHeadValues.push(this.translate("DEPARTURE"));
342 }
343 else {
344 tableHeadValues.push(this.translate("ARRIVAL"));
345 }
346
347 if (
348 this.checkDelayExist(departures)
349 || this.checkCancelledExist(departures)
350 ) {
351 const delayClockIcon = "<i class=\"fa fa-clock-o\"></i>";
352 tableHeadValues.push(delayClockIcon);
353 }
354
355 if (this.config.showDelayMsg && this.checkMsgExist(departures)) {
356 tableHeadValues.push(this.translate("DELAYMSG"));
357 }
358
359 for (
360 let thCounter = 0;
361 thCounter < tableHeadValues.length;
362 thCounter += 1
363 ) {
364 const tableHeadSetup = document.createElement("th");
365 if (thCounter === 5) {
366 tableHeadSetup.style.textAlign = "Left";
367 }
368 tableHeadSetup.innerHTML = tableHeadValues[thCounter];
369 tableHead.appendChild(tableHeadSetup);
370 }
371 return tableHead;
372 },
373
374 /**
375 * @param usableResults
376 * @param tableWrapper
377 * @returns {HTMLTableRowElement}
378 */
379 createTableContent(departures, tableWrapper) {
380 // Show the generation of the content has started
381 this.sendNotification("GENERATING_TABLE_CONTENT", { message: '[MMM-DBF] Table content is being generated.'});
382 let size = this.getSize(departures);
383 let departureCount = 0;
384 for (let index = 0; index < size; index += 1) {
385 const obj = departures[index];
386 const trWrapper = document.createElement("tr");
387 trWrapper.className = obj.isCancelled ? "tr cancelled" : "tr";
388 this.checkMsgExist(obj);
389
390 // Check train
391 if (this.config.train !== "" && !this.checkTrain(obj)) {
392 if (size + 1 <= departures.length) {
393 size += 1;
394 }
395 }
396 else if (
397 this.config.withoutDestination !== ""
398 && this.checkDestination(obj, this.config.withoutDestination)
399 ) {
400 if (size + 1 <= departures.length) {
401 size += 1;
402 }
403 }
404 else if (
405 this.config.onlyDestination !== ""
406 && !this.checkDestination(obj, this.config.onlyDestination)
407 ) {
408 if (size + 1 <= departures.length) {
409 size += 1;
410 }
411 }
412 else {
413 const tdValues = [obj.train, obj.platform, obj.destination];
414 if (this.config.via !== "") {
415 const via = this.getViaFromRoute(obj);
416 if (via === false) {
417 tdValues.push("");
418 }
419 else {
420 tdValues.push(this.getViaFromRoute(obj));
421 }
422 }
423
424 let time;
425 if (this.config.onlyArrivalTime) {
426 time = obj.scheduledArrival;
427 }
428 else {
429 time = obj.scheduledDeparture;
430 }
431
432 const remainingTime = this.convertTime(time);
433 switch (this.config.timeOption) {
434 case "time+countdown":
435 tdValues.push(`${time} (${remainingTime})`);
436 break;
437 case "countdown":
438 tdValues.push(remainingTime);
439 break;
440 default:
441 tdValues.push(time);
442 break;
443 }
444
445 if (
446 this.checkDelayExist(departures)
447 || this.checkCancelledExist(departures)
448 ) {
449 if (obj.delayDeparture > 0 && !this.config.hideLowDelay) {
450 let delay = " +" + obj.delayDeparture;
451 tdValues.push(delay);
452 }
453 else if (obj.delayDeparture >= 5) {
454 let delay = " +" + obj.delayDeparture;
455 tdValues.push(delay);
456 }
457 else if (obj.isCancelled > 0) {
458 tdValues.push(this.translate("CANCELMSG"));
459 }
460 }
461
462 if (
463 this.config.showDelayMsg
464 && this.checkMsgExist(departures)
465 && obj.delayDeparture > 0
466 ) {
467 if (obj.messages.delay.length > 0) {
468 tdValues.push(obj.messages.delay[0].text);
469 }
470 }
471
472 departureCount += 1;
473 for (let c = 0; c < tdValues.length; c += 1) {
474 const tdWrapper = document.createElement("td");
475 tdWrapper.innerHTML = tdValues[c];
476
477 if (c === this.getColDelay()) {
478 tdWrapper.className = "delay";
479 }
480 if (c === this.getColDelay() + 1) {
481 tdWrapper.className = "delay";
482 tdWrapper.style.width = "200px";
483 tdWrapper.style.textAlign = "Left";
484 // tdWrapper.innerHTML = '<marquee scrollamount="3" >' + tdValues[c] + '<marquee>';
485 }
486 trWrapper.appendChild(tdWrapper);
487 }
488 tableWrapper.appendChild(trWrapper);
489 }
490 }
491
492 if (departureCount === 0) {
493 const trWrapper = document.createElement("tr");
494 trWrapper.className = "tr";
495 const tdWrapper = document.createElement("td");
496
497 if (this.config.onlyDestination !== "" && this.config.train !== "") {
498 tdWrapper.innerHTML = "Destination or train not found";
499 Log.error("Destination or train not found");
500 }
501 else if (this.config.onlyDestination !== "") {
502 tdWrapper.innerHTML = "Destination not found";
503 Log.error("Destination not found");
504 }
505 else if (this.config.train !== "") {
506 tdWrapper.innerHTML = "Train not found";
507 Log.error("Train not found");
508 }
509
510 trWrapper.appendChild(tdWrapper);
511 tableWrapper.appendChild(trWrapper);
512 }
513 },
514
515 /**
516 * @description Define required styles.
517 * @returns {[string,string]}
518 */
519 getStyles() {
520 return ["MMM-DBF.css", "font-awesome.css"];
521 },
522
523 /**
524 * @description Load translations files
525 * @returns {{en: string, de: string}}
526 */
527 getTranslations() {
528 return {
529 en: "translations/en.json",
530 de: "translations/de.json",
531 };
532 },
533 /**
534 * @description Update data and send notification to node_helper
535 * @param {*} data
536 */
537 processData(data) {
538 // Show that data are being processed
539 this.sendNotification("PROCESSING_DATA", { message: '[MMM-DBF] Data are being processed.'});
540 this.dataRequest = data;
541
542 if (this.loaded === false) {
543 this.sendNotification("UPDATE_CONFIG_ANIMATION_SPEED", { message: '[MMM-DBF] Config animation speed is being updated.'});
544 this.updateDom(this.config.animationSpeed);
545 }
546 this.loaded = true;
547 },
548});
549