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