iobroker.jetframe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -0
  3. package/admin/SF-Pro.ttf +0 -0
  4. package/admin/admin.d.ts +65 -0
  5. package/admin/frame.html +982 -0
  6. package/admin/frame.html.bak-aircraft-card-real-row-20260518-1608 +1236 -0
  7. package/admin/frame.html.bak-aircraft-card-structure-20260518-1517 +1236 -0
  8. package/admin/frame.html.bak-aircraft-logo-id-fix-20260518-1639 +1239 -0
  9. package/admin/frame.html.bak-shortcut-test +1236 -0
  10. package/admin/frame.html.bak-tablet-class-20260518-1729 +1239 -0
  11. package/admin/heatmap.html +216 -0
  12. package/admin/index.html +268 -0
  13. package/admin/index_m.html +1749 -0
  14. package/admin/jetframe.css +1260 -0
  15. package/admin/jetframe.css.bak-airbus-landscape-fix +4630 -0
  16. package/admin/jetframe.css.bak-aircraft-card-clean-equal-20260518-1438 +4899 -0
  17. package/admin/jetframe.css.bak-aircraft-card-real-row-20260518-1608 +4814 -0
  18. package/admin/jetframe.css.bak-aircraft-card-row-left-20260518-1525 +4604 -0
  19. package/admin/jetframe.css.bak-aircraft-card-slim-equal-20260518-1446 +4647 -0
  20. package/admin/jetframe.css.bak-aircraft-card-structure-20260518-1517 +4646 -0
  21. package/admin/jetframe.css.bak-aircraft-inline-final-20260518-1527 +4654 -0
  22. package/admin/jetframe.css.bak-aircraft-row-compact-fix-20260518-1639 +4763 -0
  23. package/admin/jetframe.css.bak-before-aircrafttype-purge +4818 -0
  24. package/admin/jetframe.css.bak-before-cleanup +4670 -0
  25. package/admin/jetframe.css.bak-before-remove-tablet-only-20260518-1711 +4896 -0
  26. package/admin/jetframe.css.bak-before-tablet-layout-rework-20260518-1650 +4914 -0
  27. package/admin/jetframe.css.bak-clean-duplicate-fonts-20260518-1340 +4975 -0
  28. package/admin/jetframe.css.bak-clean-old-index-fix-20260518-1937 +5167 -0
  29. package/admin/jetframe.css.bak-hardleft-airbus +4751 -0
  30. package/admin/jetframe.css.bak-index-iphone-landscape-20260518-1931 +5030 -0
  31. package/admin/jetframe.css.bak-index-landscape-final-20260518-1941 +5167 -0
  32. package/admin/jetframe.css.bak-index-landscape-real-20260518-1936 +5186 -0
  33. package/admin/jetframe.css.bak-landscape-compact-jumbo-bold-20260518-1343 +4802 -0
  34. package/admin/jetframe.css.bak-logo-align-final +4551 -0
  35. package/admin/jetframe.css.bak-logo-final2 +4551 -0
  36. package/admin/jetframe.css.bak-narrowbody-font-fix +4992 -0
  37. package/admin/jetframe.css.bak-nuke-airbus-align +4790 -0
  38. package/admin/jetframe.css.bak-pill-balance-20260518-1603 +4773 -0
  39. package/admin/jetframe.css.bak-pill-balance-fix +4910 -0
  40. package/admin/jetframe.css.bak-radar-fix-fonts +4710 -0
  41. package/admin/jetframe.css.bak-shortcut-test +4899 -0
  42. package/admin/jetframe.css.bak-smaller-aircraft-card-fonts-20260518-1345 +4897 -0
  43. package/admin/jetframe.css.bak-tablet-fix-real-20260518-1748 +4945 -0
  44. package/admin/jetframe.css.bak-tablet-fullscreen-fix-20260518-1804 +4972 -0
  45. package/admin/jetframe.css.bak-tablet-landscape-layout-20260518-1645 +4802 -0
  46. package/admin/jetframe.css.bak-tablet-layout-final-20260518-1839 +4802 -0
  47. package/admin/jetframe.css.bak-tablet-layout-v3-20260518-1729 +4802 -0
  48. package/admin/jetframe.css.bak-tablet-layout-v4-20260518-1801 +4957 -0
  49. package/admin/jetframe.css.bak-tablet-layout-v5-20260518-1843 +4970 -0
  50. package/admin/jetframe.css.bak-tablet-layout-v6-20260518-1848 +4958 -0
  51. package/admin/jetframe.css.bak-tablet-layout-v7-20260518-1909 +4985 -0
  52. package/admin/jetframe.css.bak-tablet-only-landscape-v2-20260518-1707 +4802 -0
  53. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1857 +5188 -0
  54. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1859 +5347 -0
  55. package/admin/jetframe.css.bak-tablet-pages-v2-20260519-190807 +5349 -0
  56. package/admin/jetframe.css.bak-typography-align-final +4818 -0
  57. package/admin/jetframe.png +0 -0
  58. package/admin/manifest.webmanifest +15 -0
  59. package/admin/src/app.tsx +58 -0
  60. package/admin/src/components/settings.tsx +97 -0
  61. package/admin/src/i18n/de.json +11 -0
  62. package/admin/src/i18n/en.json +11 -0
  63. package/admin/src/i18n/es.json +11 -0
  64. package/admin/src/i18n/fr.json +11 -0
  65. package/admin/src/i18n/i18n.d.ts +28 -0
  66. package/admin/src/i18n/it.json +11 -0
  67. package/admin/src/i18n/nl.json +11 -0
  68. package/admin/src/i18n/pl.json +11 -0
  69. package/admin/src/i18n/pt.json +11 -0
  70. package/admin/src/i18n/ru.json +11 -0
  71. package/admin/src/i18n/uk.json +11 -0
  72. package/admin/src/i18n/zh-cn.json +11 -0
  73. package/admin/src/index.tsx +25 -0
  74. package/admin/stats.html +228 -0
  75. package/admin/style.css +32 -0
  76. package/admin/tsconfig.json +11 -0
  77. package/admin/words.js +46 -0
  78. package/build/lib/adsb.js +218 -0
  79. package/build/lib/adsb.js.map +7 -0
  80. package/build/lib/airportNamesDe.js +131 -0
  81. package/build/lib/airportNamesDe.js.map +7 -0
  82. package/build/lib/airports.js +281 -0
  83. package/build/lib/airports.js.map +7 -0
  84. package/build/lib/classify.js +339 -0
  85. package/build/lib/classify.js.map +7 -0
  86. package/build/lib/config.js +103 -0
  87. package/build/lib/config.js.map +7 -0
  88. package/build/lib/flightInfo.js +1409 -0
  89. package/build/lib/flightInfo.js.map +7 -0
  90. package/build/lib/geo.js +84 -0
  91. package/build/lib/geo.js.map +7 -0
  92. package/build/lib/images.js +422 -0
  93. package/build/lib/images.js.map +7 -0
  94. package/build/lib/specialLiveries.js +342 -0
  95. package/build/lib/specialLiveries.js.map +7 -0
  96. package/build/lib/states.js +971 -0
  97. package/build/lib/states.js.map +7 -0
  98. package/build/lib/staticFiles.js +73 -0
  99. package/build/lib/staticFiles.js.map +7 -0
  100. package/build/lib/types.js +17 -0
  101. package/build/lib/types.js.map +7 -0
  102. package/build/lib/visConfig.js +52 -0
  103. package/build/lib/visConfig.js.map +7 -0
  104. package/build/main.js +1454 -0
  105. package/build/main.js.map +7 -0
  106. package/io-package.json +169 -0
  107. package/package.json +82 -0
package/build/main.js ADDED
@@ -0,0 +1,1454 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var utils = __toESM(require("@iobroker/adapter-core"));
25
+ var https = __toESM(require("https"));
26
+ var http = __toESM(require("http"));
27
+ var import_staticFiles = require("./lib/staticFiles");
28
+ var import_airports = require("./lib/airports");
29
+ var import_specialLiveries = require("./lib/specialLiveries");
30
+ var import_config = require("./lib/config");
31
+ var import_adsb = require("./lib/adsb");
32
+ var import_classify = require("./lib/classify");
33
+ var import_states = require("./lib/states");
34
+ var import_images = require("./lib/images");
35
+ var import_visConfig = require("./lib/visConfig");
36
+ var import_flightInfo = require("./lib/flightInfo");
37
+ class Jetframe extends utils.Adapter {
38
+ timer = null;
39
+ statisticsTimer = null;
40
+ liveTarget = null;
41
+ liveInfo = null;
42
+ liveStarted = 0;
43
+ lastStartKey = "";
44
+ lastStartTs = 0;
45
+ lastImageSaveKey = "";
46
+ lastIdleRunwayText = "";
47
+ constructor(options = {}) {
48
+ super({
49
+ ...options,
50
+ name: "jetframe"
51
+ });
52
+ this.on("ready", this.onReady.bind(this));
53
+ this.on("unload", this.onUnload.bind(this));
54
+ this.on("stateChange", this.onStateChange.bind(this));
55
+ }
56
+ async onReady() {
57
+ this.log.info("JetFrame Adapter gestartet");
58
+ await (0, import_staticFiles.copyStaticFiles)(this);
59
+ try {
60
+ const config = (0, import_config.readConfig)(this);
61
+ await (0, import_states.ensureStates)(this, config);
62
+ await this.ensureStatisticsStates(config.dpRoot);
63
+ await this.resetTodayStatisticsIfNeeded(`${config.dpRoot}.statistics`);
64
+ this.scheduleStatisticsRotation(config.dpRoot);
65
+ await this.ensureProbableRunwayStates(config.dpRoot);
66
+ await this.ensureIdleRunwayState(config.dpRoot);
67
+ this.subscribeStates("clearImageCache");
68
+ this.log.debug("[JetFrame] States OK");
69
+ await (0, import_images.ensureImageDirs)(this, this.logDebug.bind(this), this.logWarn.bind(this));
70
+ await (0, import_visConfig.writeVisConfig)(this, this.config, this.logDebug.bind(this), this.logWarn.bind(this));
71
+ this.log.debug("[JetFrame] Images OK");
72
+ (0, import_airports.updateAirportJson)(this, this.logDebug.bind(this), this.logWarn.bind(this)).catch((e) => {
73
+ this.logWarn(`Airport DB Update Fehler: ${this.errorText(e)}`);
74
+ });
75
+ (0, import_specialLiveries.updateSpecialLiveries)(this, this.logDebug.bind(this), this.logWarn.bind(this)).catch((e) => {
76
+ this.logWarn(`Special-Liveries DB Update Fehler: ${this.errorText(e)}`);
77
+ });
78
+ this.log.debug("[JetFrame] Starte Loop");
79
+ this.loop().catch((e) => {
80
+ this.logError(`Loop Start Fehler: ${this.errorText(e)}`);
81
+ });
82
+ } catch (e) {
83
+ this.logError(`onReady Fehler: ${this.errorText(e)}`);
84
+ }
85
+ }
86
+ async onStateChange(id, state) {
87
+ if (!state || state.ack || state.val !== true) {
88
+ return;
89
+ }
90
+ const config = (0, import_config.readConfig)(this);
91
+ if (id !== `${config.dpRoot}.clearImageCache`) {
92
+ return;
93
+ }
94
+ try {
95
+ await (0, import_images.clearImageCache)(this, this.logDebug.bind(this), this.logWarn.bind(this));
96
+ await this.setForeignStateAsync(`${config.dpRoot}.clearImageCache`, false, true);
97
+ this.log.info("JetFrame Bild-/Logo-Cache wurde geleert");
98
+ } catch (e) {
99
+ this.logWarn(`Cache leeren fehlgeschlagen: ${this.errorText(e)}`);
100
+ await this.setForeignStateAsync(`${config.dpRoot}.clearImageCache`, false, true);
101
+ }
102
+ }
103
+ async ensureIdleRunwayState(dpRoot) {
104
+ try {
105
+ await this.setForeignObjectNotExistsAsync(`${dpRoot}.idleRunwayText`, {
106
+ type: "state",
107
+ common: {
108
+ name: "Idle active runway text",
109
+ type: "string",
110
+ role: "text",
111
+ read: true,
112
+ write: false
113
+ },
114
+ native: {}
115
+ });
116
+ } catch (e) {
117
+ this.logWarn(`idleRunwayText State konnte nicht erstellt werden: ${this.errorText(e)}`);
118
+ }
119
+ }
120
+ async updateIdleRunway(a, config) {
121
+ var _a;
122
+ const track = Number(a.trackDeg || a.track || 0);
123
+ if (!Number.isFinite(track) || !track) {
124
+ return;
125
+ }
126
+ let runways = [];
127
+ try {
128
+ const st = await this.getForeignStateAsync(`${config.dpRoot}.airportjson`);
129
+ const airports = JSON.parse(String((st == null ? void 0 : st.val) || "[]"));
130
+ const airportIata = String(((_a = config.airport) == null ? void 0 : _a.iata) || "").trim().toUpperCase();
131
+ const airport = Array.isArray(airports) ? airports.find(
132
+ (x) => String(x.iata || x.IATA || "").trim().toUpperCase() === airportIata
133
+ ) : null;
134
+ runways = Array.isArray(airport == null ? void 0 : airport.runways) ? airport.runways : [];
135
+ } catch {
136
+ runways = [];
137
+ }
138
+ let bestName = "";
139
+ let bestGroup = "";
140
+ let bestDiff = 999;
141
+ for (const rw of runways) {
142
+ const sides = [
143
+ { name: rw.leIdent, heading: rw.leHeadingDeg },
144
+ { name: rw.heIdent, heading: rw.heHeadingDeg }
145
+ ];
146
+ for (const side of sides) {
147
+ const name = String(side.name || "").trim().toUpperCase();
148
+ const heading = Number(side.heading);
149
+ if (!name || !Number.isFinite(heading)) {
150
+ continue;
151
+ }
152
+ let diff = Math.abs(track - heading);
153
+ if (diff > 180) {
154
+ diff = 360 - diff;
155
+ }
156
+ if (diff < bestDiff) {
157
+ bestDiff = diff;
158
+ bestName = name;
159
+ bestGroup = name.replace(/[LCR]$/i, "");
160
+ }
161
+ }
162
+ }
163
+ let runway = "";
164
+ if (bestName && bestDiff <= 35) {
165
+ runway = `RWY ${bestGroup || bestName} aktiv`;
166
+ } else {
167
+ runway = `Aktive Richtung ${Math.round(track)}\xB0`;
168
+ }
169
+ const mode = a.mode === "LANDING" ? "Landungen" : a.mode === "TAKEOFF" ? "Starts" : "Traffic";
170
+ const text = `${runway} \xB7 ${mode}`;
171
+ if (text === this.lastIdleRunwayText) {
172
+ return;
173
+ }
174
+ this.lastIdleRunwayText = text;
175
+ await this.setForeignStateAsync(`${config.dpRoot}.idleRunwayText`, text, true);
176
+ }
177
+ async ensureProbableRunwayStates(dpRoot) {
178
+ const bases = [`${dpRoot}.current`, `${dpRoot}.airport`, `${dpRoot}.overflight`];
179
+ for (const base of bases) {
180
+ await this.ensureSimpleState(`${base}.probableRunway`, "Probable runway", "string", "text");
181
+ await this.ensureSimpleState(`${base}.probableRunwayText`, "Probable runway text", "string", "text");
182
+ await this.ensureSimpleState(`${base}.probableRunwayHeading`, "Probable runway heading", "number", "value");
183
+ await this.ensureSimpleState(
184
+ `${base}.probableRunwayDiffDeg`,
185
+ "Probable runway difference degrees",
186
+ "number",
187
+ "value"
188
+ );
189
+ }
190
+ }
191
+ async ensureSimpleState(id, name, type, role) {
192
+ try {
193
+ await this.setForeignObjectNotExistsAsync(id, {
194
+ type: "state",
195
+ common: {
196
+ name,
197
+ type,
198
+ role,
199
+ read: true,
200
+ write: false
201
+ },
202
+ native: {}
203
+ });
204
+ const st = await this.getForeignStateAsync(id);
205
+ if (!st) {
206
+ await this.setForeignStateAsync(id, type === "number" ? 0 : type === "boolean" ? false : "", true);
207
+ }
208
+ } catch (e) {
209
+ this.logWarn(`State konnte nicht erstellt/initialisiert werden: ${id} | ${this.errorText(e)}`);
210
+ }
211
+ }
212
+ async applyProbableRunway(a, config) {
213
+ var _a;
214
+ const track = Number(a.trackDeg || a.track || 0);
215
+ if (!Number.isFinite(track) || !track) {
216
+ return;
217
+ }
218
+ let runways = [];
219
+ try {
220
+ const st = await this.getForeignStateAsync(`${config.dpRoot}.airportjson`);
221
+ const airports = JSON.parse(String((st == null ? void 0 : st.val) || "[]"));
222
+ const airportIata = String(((_a = config.airport) == null ? void 0 : _a.iata) || "").trim().toUpperCase();
223
+ const airport = Array.isArray(airports) ? airports.find(
224
+ (x) => String(x.iata || x.IATA || "").trim().toUpperCase() === airportIata
225
+ ) : null;
226
+ runways = Array.isArray(airport == null ? void 0 : airport.runways) ? airport.runways : [];
227
+ } catch {
228
+ runways = [];
229
+ }
230
+ let bestName = "";
231
+ let bestGroup = "";
232
+ let bestHeading = 0;
233
+ let bestDiff = 999;
234
+ for (const rw of runways) {
235
+ const sides = [
236
+ { name: rw.leIdent, heading: rw.leHeadingDeg },
237
+ { name: rw.heIdent, heading: rw.heHeadingDeg }
238
+ ];
239
+ for (const side of sides) {
240
+ const name = String(side.name || "").trim().toUpperCase();
241
+ const heading = Number(side.heading);
242
+ if (!name || !Number.isFinite(heading)) {
243
+ continue;
244
+ }
245
+ let diff = Math.abs(track - heading);
246
+ if (diff > 180) {
247
+ diff = 360 - diff;
248
+ }
249
+ if (diff < bestDiff) {
250
+ bestDiff = diff;
251
+ bestName = name;
252
+ bestGroup = name.replace(/[LCR]$/i, "");
253
+ bestHeading = heading;
254
+ }
255
+ }
256
+ }
257
+ if (!bestName || bestDiff > 35) {
258
+ a.probableRunway = "";
259
+ a.probableRunwayText = "";
260
+ a.probableRunwayHeading = 0;
261
+ a.probableRunwayDiffDeg = 0;
262
+ a.runwayConfidence = 0;
263
+ return;
264
+ }
265
+ const modeIcon = a.mode === "LANDING" ? "\u{1F6EC}" : a.mode === "TAKEOFF" ? "\u{1F6EB}" : "\u{1F4E1}";
266
+ const modeText = a.mode === "LANDING" ? "Landung" : a.mode === "TAKEOFF" ? "Start" : "Traffic";
267
+ const confidence = Math.max(0, Math.min(100, Math.round(100 - bestDiff / 35 * 100)));
268
+ a.probableRunway = bestGroup || bestName;
269
+ a.probableRunwayHeading = Math.round(bestHeading);
270
+ a.probableRunwayDiffDeg = Math.round(bestDiff);
271
+ a.runwayConfidence = confidence;
272
+ a.probableRunwayText = `${modeIcon} vermutlich RWY ${bestGroup || bestName} \xB7 ${modeText}`;
273
+ }
274
+ async ensureStatisticsStates(dpRoot) {
275
+ const base = `${dpRoot}.statistics`;
276
+ await this.ensureSimpleState(`${base}.totalFlightsSeen`, "Total flights seen", "number", "value");
277
+ await this.ensureSimpleState(`${base}.landings`, "Landings", "number", "value");
278
+ await this.ensureSimpleState(`${base}.departures`, "Departures", "number", "value");
279
+ await this.ensureSimpleState(`${base}.overflights`, "Overflights", "number", "value");
280
+ await this.ensureSimpleState(`${base}.lastFlight`, "Last flight", "string", "text");
281
+ await this.ensureSimpleState(`${base}.lastAirline`, "Last airline", "string", "text");
282
+ await this.ensureSimpleState(`${base}.lastRoute`, "Last route", "string", "text");
283
+ await this.ensureSimpleState(`${base}.lastRegistration`, "Last registration", "string", "text");
284
+ await this.ensureSimpleState(`${base}.lastSeen`, "Last seen", "string", "text");
285
+ await this.ensureSimpleState(`${base}.airlineRanking`, "Airline ranking JSON", "string", "json");
286
+ await this.ensureSimpleState(`${base}.airlineRankingText`, "Airline ranking text", "string", "text");
287
+ await this.ensureSimpleState(`${base}.aircraftTypeRanking`, "Aircraft type ranking JSON", "string", "json");
288
+ await this.ensureSimpleState(`${base}.aircraftTypeRankingText`, "Aircraft type ranking text", "string", "text");
289
+ await this.ensureSimpleState(`${base}.registrationRanking`, "Registration ranking JSON", "string", "json");
290
+ await this.ensureSimpleState(`${base}.registrationRankingText`, "Registration ranking text", "string", "text");
291
+ await this.ensureSimpleState(`${base}.runwayUsageRanking`, "Runway usage ranking JSON", "string", "json");
292
+ await this.ensureSimpleState(`${base}.runwayUsageRankingText`, "Runway usage ranking text", "string", "text");
293
+ await this.ensureSimpleState(`${base}.airlineRunwayRanking`, "Airline runway ranking JSON", "string", "json");
294
+ await this.ensureSimpleState(
295
+ `${base}.airlineRunwayRankingText`,
296
+ "Airline runway ranking text",
297
+ "string",
298
+ "text"
299
+ );
300
+ await this.ensureSimpleState(`${base}.flightLogHistory`, "Flight log history JSON", "string", "json");
301
+ await this.ensureSimpleState(`${base}.flightLogHistoryText`, "Flight log history text", "string", "text");
302
+ await this.ensureSimpleState(`${base}.today.date`, "Today date", "string", "text");
303
+ await this.ensureSimpleState(`${base}.today.totalFlights`, "Today total flights", "number", "value");
304
+ await this.ensureSimpleState(`${base}.today.landings`, "Today landings", "number", "value");
305
+ await this.ensureSimpleState(`${base}.today.departures`, "Today departures", "number", "value");
306
+ await this.ensureSimpleState(`${base}.today.overflights`, "Today overflights", "number", "value");
307
+ await this.ensureSimpleState(`${base}.today.firstSeen`, "Today first seen", "string", "text");
308
+ await this.ensureSimpleState(`${base}.today.lastSeen`, "Today last seen", "string", "text");
309
+ await this.ensureSimpleState(`${base}.today.lastFlight`, "Today last flight", "string", "text");
310
+ await this.ensureSimpleState(`${base}.today.lastAirline`, "Today last airline", "string", "text");
311
+ await this.ensureSimpleState(`${base}.today.lastRoute`, "Today last route", "string", "text");
312
+ await this.ensureSimpleState(`${base}.today.lastRegistration`, "Today last registration", "string", "text");
313
+ await this.ensureSimpleState(`${base}.today.topAirline`, "Today top airline", "string", "text");
314
+ await this.ensureSimpleState(`${base}.today.topRoute`, "Today top route", "string", "text");
315
+ await this.ensureSimpleState(`${base}.today.airlineRanking`, "Today airline ranking JSON", "string", "json");
316
+ await this.ensureSimpleState(
317
+ `${base}.today.airlineRankingText`,
318
+ "Today airline ranking text",
319
+ "string",
320
+ "text"
321
+ );
322
+ await this.ensureSimpleState(`${base}.today.routeRanking`, "Today route ranking JSON", "string", "json");
323
+ await this.ensureSimpleState(`${base}.today.routeRankingText`, "Today route ranking text", "string", "text");
324
+ await this.ensureSimpleState(`${base}.today.hourly`, "Today hourly flights JSON", "string", "json");
325
+ await this.ensureSimpleState(`${base}.today.hourlyText`, "Today hourly flights text", "string", "text");
326
+ await this.ensureSimpleState(`${base}.today.bestSpotterHour`, "Today best spotter hour", "string", "text");
327
+ await this.ensureSimpleState(
328
+ `${base}.today.currentHourFlights`,
329
+ "Today current hour flights",
330
+ "number",
331
+ "value"
332
+ );
333
+ await this.ensureSimpleState(`${base}.today.rushHourNow`, "Rush hour now", "boolean", "indicator");
334
+ await this.ensureSimpleState(`${base}.today.rushHourText`, "Rush hour text", "string", "text");
335
+ await this.ensureSimpleState(`${base}.today.a380Count`, "Today A380 count", "number", "value");
336
+ await this.ensureSimpleState(`${base}.today.b747Count`, "Today B747 count", "number", "value");
337
+ await this.ensureSimpleState(
338
+ `${base}.today.heavyAircraftCount`,
339
+ "Today heavy aircraft count",
340
+ "number",
341
+ "value"
342
+ );
343
+ await this.ensureSimpleState(
344
+ `${base}.today.specialLiveryCount`,
345
+ "Today special livery count",
346
+ "number",
347
+ "value"
348
+ );
349
+ await this.ensureSimpleState(`${base}.yesterday.date`, "Yesterday date", "string", "text");
350
+ await this.ensureSimpleState(`${base}.yesterday.totalFlights`, "Yesterday total flights", "number", "value");
351
+ await this.ensureSimpleState(`${base}.yesterday.landings`, "Yesterday landings", "number", "value");
352
+ await this.ensureSimpleState(`${base}.yesterday.departures`, "Yesterday departures", "number", "value");
353
+ await this.ensureSimpleState(`${base}.yesterday.overflights`, "Yesterday overflights", "number", "value");
354
+ await this.ensureSimpleState(
355
+ `${base}.yesterday.bestSpotterHour`,
356
+ "Yesterday best spotter hour",
357
+ "string",
358
+ "text"
359
+ );
360
+ await this.ensureSimpleState(
361
+ `${base}.yesterday.bestHourFlights`,
362
+ "Yesterday best hour flights",
363
+ "number",
364
+ "value"
365
+ );
366
+ await this.ensureSimpleState(`${base}.yesterday.topAirline`, "Yesterday top airline", "string", "text");
367
+ await this.ensureSimpleState(`${base}.yesterday.topRoute`, "Yesterday top route", "string", "text");
368
+ await this.ensureSimpleState(`${base}.yesterday.a380Count`, "Yesterday A380 count", "number", "value");
369
+ await this.ensureSimpleState(`${base}.yesterday.b747Count`, "Yesterday B747 count", "number", "value");
370
+ await this.ensureSimpleState(
371
+ `${base}.yesterday.heavyAircraftCount`,
372
+ "Yesterday heavy aircraft count",
373
+ "number",
374
+ "value"
375
+ );
376
+ await this.ensureSimpleState(
377
+ `${base}.yesterday.specialLiveryCount`,
378
+ "Yesterday special livery count",
379
+ "number",
380
+ "value"
381
+ );
382
+ await this.ensureSimpleState(`${base}.yesterday.hourly`, "Yesterday hourly JSON", "string", "json");
383
+ await this.ensureSimpleState(
384
+ `${base}.yesterday.airlineRankingText`,
385
+ "Yesterday airline ranking text",
386
+ "string",
387
+ "text"
388
+ );
389
+ await this.ensureSimpleState(
390
+ `${base}.yesterday.routeRankingText`,
391
+ "Yesterday route ranking text",
392
+ "string",
393
+ "text"
394
+ );
395
+ await this.ensureSimpleState(`${base}.history.daily`, "Daily statistics history JSON", "string", "json");
396
+ await this.ensureSimpleState(`${base}.history.dailyText`, "Daily statistics history text", "string", "text");
397
+ await this.ensureSimpleState(`${base}.alltime.bestDayDate`, "Alltime best day date", "string", "text");
398
+ await this.ensureSimpleState(`${base}.alltime.bestDayFlights`, "Alltime best day flights", "number", "value");
399
+ await this.ensureSimpleState(`${base}.alltime.bestHour`, "Alltime best hour", "string", "text");
400
+ await this.ensureSimpleState(`${base}.alltime.bestHourFlights`, "Alltime best hour flights", "number", "value");
401
+ await this.ensureSimpleState(`${base}.alltime.bestAirline`, "Alltime best airline", "string", "text");
402
+ await this.ensureSimpleState(`${base}.alltime.bestRoute`, "Alltime best route", "string", "text");
403
+ await this.ensureSimpleState(
404
+ `${base}.alltime.bestAircraftType`,
405
+ "Alltime best aircraft type",
406
+ "string",
407
+ "text"
408
+ );
409
+ await this.ensureSimpleState(`${base}.alltime.a380Count`, "Alltime A380 count", "number", "value");
410
+ await this.ensureSimpleState(`${base}.alltime.b747Count`, "Alltime B747 count", "number", "value");
411
+ await this.ensureSimpleState(
412
+ `${base}.alltime.heavyAircraftCount`,
413
+ "Alltime heavy aircraft count",
414
+ "number",
415
+ "value"
416
+ );
417
+ await this.ensureSimpleState(
418
+ `${base}.alltime.specialLiveryCount`,
419
+ "Alltime special livery count",
420
+ "number",
421
+ "value"
422
+ );
423
+ await this.ensureSimpleState(
424
+ `${base}.alltime.bestSpecialDayDate`,
425
+ "Alltime best special day date",
426
+ "string",
427
+ "text"
428
+ );
429
+ await this.ensureSimpleState(
430
+ `${base}.alltime.bestSpecialDayCount`,
431
+ "Alltime best special day count",
432
+ "number",
433
+ "value"
434
+ );
435
+ await this.ensureSimpleState(`${base}.alltime.hourly`, "Alltime hourly JSON", "string", "json");
436
+ await this.ensureSimpleState(
437
+ `${base}.alltime.airlineRanking`,
438
+ "Alltime airline ranking JSON",
439
+ "string",
440
+ "json"
441
+ );
442
+ await this.ensureSimpleState(
443
+ `${base}.alltime.airlineRankingText`,
444
+ "Alltime airline ranking text",
445
+ "string",
446
+ "text"
447
+ );
448
+ await this.ensureSimpleState(`${base}.alltime.routeRanking`, "Alltime route ranking JSON", "string", "json");
449
+ await this.ensureSimpleState(
450
+ `${base}.alltime.routeRankingText`,
451
+ "Alltime route ranking text",
452
+ "string",
453
+ "text"
454
+ );
455
+ }
456
+ async readNumberState(id) {
457
+ try {
458
+ const st = await this.getForeignStateAsync(id);
459
+ const n = Number((st == null ? void 0 : st.val) || 0);
460
+ return Number.isFinite(n) ? n : 0;
461
+ } catch {
462
+ return 0;
463
+ }
464
+ }
465
+ async readJsonState(id, fallback) {
466
+ try {
467
+ const st = await this.getForeignStateAsync(id);
468
+ const raw = String((st == null ? void 0 : st.val) || "").trim();
469
+ if (!raw) {
470
+ return fallback;
471
+ }
472
+ return JSON.parse(raw);
473
+ } catch {
474
+ return fallback;
475
+ }
476
+ }
477
+ topRankingText(entries, limit = 5) {
478
+ return entries.slice(0, limit).map(([name, count], index) => `${index + 1}. ${name} \xB7 ${count}`).join("\n");
479
+ }
480
+ sortedRanking(data, limit = 30) {
481
+ return Object.entries(data).filter(([name]) => !!this.clean(name)).sort((aEntry, bEntry) => bEntry[1] - aEntry[1] || aEntry[0].localeCompare(bEntry[0])).slice(0, limit);
482
+ }
483
+ async incrementRankingState(id, textId, key, limit = 30, textLimit = 5) {
484
+ const cleanKey = this.clean(key);
485
+ if (!cleanKey) {
486
+ return [];
487
+ }
488
+ const ranking = await this.readJsonState(id, {});
489
+ ranking[cleanKey] = (ranking[cleanKey] || 0) + 1;
490
+ const sorted = this.sortedRanking(ranking, limit);
491
+ await this.setForeignStateAsync(id, JSON.stringify(Object.fromEntries(sorted)), true);
492
+ await this.setForeignStateAsync(textId, this.topRankingText(sorted, textLimit), true);
493
+ return sorted;
494
+ }
495
+ async updateGlobalDetailedStatistics(base, a, info) {
496
+ const type = this.clean(a.aircraftTypeText) || this.clean(a.aircraftModel) || this.clean(a.aircraftType) || this.clean(a.type) || "Unbekannt";
497
+ await this.incrementRankingState(`${base}.aircraftTypeRanking`, `${base}.aircraftTypeRankingText`, type, 40, 8);
498
+ if (info.registration) {
499
+ await this.incrementRankingState(
500
+ `${base}.registrationRanking`,
501
+ `${base}.registrationRankingText`,
502
+ info.registration,
503
+ 50,
504
+ 8
505
+ );
506
+ }
507
+ const runway = this.clean(a.probableRunway || "");
508
+ const runwayConfidence = Number(a.runwayConfidence || 0);
509
+ if (runway && runwayConfidence >= 40) {
510
+ await this.incrementRankingState(
511
+ `${base}.runwayUsageRanking`,
512
+ `${base}.runwayUsageRankingText`,
513
+ runway,
514
+ 30,
515
+ 8
516
+ );
517
+ const airlineRunwayKey = `${info.airline} \u2192 RWY ${runway}`;
518
+ await this.incrementRankingState(
519
+ `${base}.airlineRunwayRanking`,
520
+ `${base}.airlineRunwayRankingText`,
521
+ airlineRunwayKey,
522
+ 50,
523
+ 8
524
+ );
525
+ }
526
+ const historyId = `${base}.flightLogHistory`;
527
+ const historyTextId = `${base}.flightLogHistoryText`;
528
+ const history = await this.readJsonState(historyId, []);
529
+ const entry = {
530
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
531
+ mode: info.mode,
532
+ phase: this.clean(a.flightPhase || ""),
533
+ callsign: info.callsign,
534
+ airline: info.airline,
535
+ route: info.route,
536
+ originIata: this.clean(a.originIata || ""),
537
+ destIata: this.clean(a.destIata || ""),
538
+ registration: info.registration,
539
+ aircraftType: type,
540
+ runway,
541
+ runwayConfidence: Number.isFinite(runwayConfidence) ? runwayConfidence : 0
542
+ };
543
+ history.unshift(entry);
544
+ const limitedHistory = history.slice(0, 200);
545
+ const historyText = limitedHistory.slice(0, 10).map((item) => {
546
+ const d = new Date(item.ts);
547
+ const hh = String(d.getHours()).padStart(2, "0");
548
+ const mm = String(d.getMinutes()).padStart(2, "0");
549
+ const icon = item.mode === "LANDING" ? "\u{1F6EC}" : item.mode === "TAKEOFF" ? "\u{1F6EB}" : "\u{1F6E9}\uFE0F";
550
+ const rw = item.runway ? ` \xB7 RWY ${item.runway}` : "";
551
+ const routeText = item.route ? ` \xB7 ${item.route}` : "";
552
+ return `${hh}:${mm} ${icon} ${item.callsign || "?"} \xB7 ${item.airline || "?"}${routeText}${rw}`;
553
+ }).join("\n");
554
+ await this.setForeignStateAsync(historyId, JSON.stringify(limitedHistory), true);
555
+ await this.setForeignStateAsync(historyTextId, historyText, true);
556
+ }
557
+ isA380(a) {
558
+ const all = [a.aircraftTypeText, a.aircraftModel, a.aircraftType, a.type].map((v) => this.clean(v).toUpperCase()).join(" ").replace(/[\s_-]/g, "");
559
+ return /A380|A388|A38/.test(all);
560
+ }
561
+ isB747(a) {
562
+ const all = [a.aircraftTypeText, a.aircraftModel, a.aircraftType, a.type].map((v) => this.clean(v).toUpperCase()).join(" ").replace(/[\s_-]/g, "");
563
+ return /B747|B741|B742|B743|B744|B748|747/.test(all);
564
+ }
565
+ isHeavyAircraft(a) {
566
+ const all = [
567
+ a.aircraftTypeText,
568
+ a.aircraftModel,
569
+ a.aircraftType,
570
+ a.type,
571
+ a.aircraftSize
572
+ ].map((v) => this.clean(v).toUpperCase()).join(" ").replace(/[\s_-]/g, "");
573
+ return /WIDEBODY|HEAVY|SUPERJUMBO|JUMBO|HEAVYCARGO/.test(all) || /A300|A310|A330|A332|A333|A338|A339|A340|A342|A343|A345|A346/.test(all) || /A350|A359|A35K|A351|A380|A388/.test(all) || /B747|B741|B742|B743|B744|B748/.test(all) || /B757|B752|B753/.test(all) || /B767|B762|B763|B764/.test(all) || /B777|B772|B773|B77W|B778|B779|B77L|B77F/.test(all) || /B787|B788|B789|B78X/.test(all) || /MD11|DC10|L1011|IL86|IL96/.test(all) || /AN124|AN225|C5M|GALAXY|C17|C17A|GLOBEMASTER|A400|A400M/.test(all);
574
+ }
575
+ isSpecialFlight(a) {
576
+ return !!(a.isSpecial || this.clean(a.specialText) || this.clean(a.specialLiveryTitle) || this.clean(a.specialLiveryDescription) || this.clean(a.specialLiveryFull) || this.clean(a.specialLiveryVisText));
577
+ }
578
+ mergeRanking(target, source) {
579
+ for (const [key, value] of Object.entries(source || {})) {
580
+ const cleanKey = this.clean(key);
581
+ if (!cleanKey) {
582
+ continue;
583
+ }
584
+ const count = Number(value || 0);
585
+ if (!Number.isFinite(count) || count <= 0) {
586
+ continue;
587
+ }
588
+ target[cleanKey] = (target[cleanKey] || 0) + count;
589
+ }
590
+ return target;
591
+ }
592
+ bestHourFromHourly(hourly) {
593
+ const best = Object.entries(hourly || {}).filter(([, value]) => Number((value == null ? void 0 : value.total) || 0) > 0).sort((a, b) => Number(b[1].total || 0) - Number(a[1].total || 0) || Number(a[0]) - Number(b[0]))[0];
594
+ return best ? { hour: `${best[0]}:00`, total: Number(best[1].total || 0) } : { hour: "", total: 0 };
595
+ }
596
+ async archiveTodayStatistics(base, storedDate) {
597
+ const todayBase = `${base}.today`;
598
+ const yesterdayBase = `${base}.yesterday`;
599
+ const totalFlights = await this.readNumberState(`${todayBase}.totalFlights`);
600
+ if (!storedDate || totalFlights <= 0) {
601
+ return;
602
+ }
603
+ const landings = await this.readNumberState(`${todayBase}.landings`);
604
+ const departures = await this.readNumberState(`${todayBase}.departures`);
605
+ const overflights = await this.readNumberState(`${todayBase}.overflights`);
606
+ const a380Count = await this.readNumberState(`${todayBase}.a380Count`);
607
+ const b747Count = await this.readNumberState(`${todayBase}.b747Count`);
608
+ const heavyAircraftCount = await this.readNumberState(`${todayBase}.heavyAircraftCount`);
609
+ const specialLiveryCount = await this.readNumberState(`${todayBase}.specialLiveryCount`);
610
+ const hourly = await this.readJsonState(`${todayBase}.hourly`, this.emptyHourlyStats());
611
+ const airlineRanking = await this.readJsonState(`${todayBase}.airlineRanking`, {});
612
+ const routeRanking = await this.readJsonState(`${todayBase}.routeRanking`, {});
613
+ const airlineSorted = this.sortedRanking(airlineRanking, 20);
614
+ const routeSorted = this.sortedRanking(routeRanking, 20);
615
+ const bestHour = this.bestHourFromHourly(hourly);
616
+ const topAirline = airlineSorted.length ? `${airlineSorted[0][0]} \xB7 ${airlineSorted[0][1]}` : "";
617
+ const topRoute = routeSorted.length ? `${routeSorted[0][0]} \xB7 ${routeSorted[0][1]}` : "";
618
+ await this.setForeignStateAsync(`${yesterdayBase}.date`, storedDate, true);
619
+ await this.setForeignStateAsync(`${yesterdayBase}.totalFlights`, totalFlights, true);
620
+ await this.setForeignStateAsync(`${yesterdayBase}.landings`, landings, true);
621
+ await this.setForeignStateAsync(`${yesterdayBase}.departures`, departures, true);
622
+ await this.setForeignStateAsync(`${yesterdayBase}.overflights`, overflights, true);
623
+ await this.setForeignStateAsync(
624
+ `${yesterdayBase}.bestSpotterHour`,
625
+ bestHour.hour ? `${bestHour.hour} \xB7 ${bestHour.total} Fl\xFCge` : "",
626
+ true
627
+ );
628
+ await this.setForeignStateAsync(`${yesterdayBase}.bestHourFlights`, bestHour.total, true);
629
+ await this.setForeignStateAsync(`${yesterdayBase}.topAirline`, topAirline, true);
630
+ await this.setForeignStateAsync(`${yesterdayBase}.topRoute`, topRoute, true);
631
+ await this.setForeignStateAsync(`${yesterdayBase}.a380Count`, a380Count, true);
632
+ await this.setForeignStateAsync(`${yesterdayBase}.b747Count`, b747Count, true);
633
+ await this.setForeignStateAsync(`${yesterdayBase}.heavyAircraftCount`, heavyAircraftCount, true);
634
+ await this.setForeignStateAsync(`${yesterdayBase}.specialLiveryCount`, specialLiveryCount, true);
635
+ await this.setForeignStateAsync(`${yesterdayBase}.hourly`, JSON.stringify(hourly), true);
636
+ await this.setForeignStateAsync(
637
+ `${yesterdayBase}.airlineRankingText`,
638
+ this.topRankingText(airlineSorted, 5),
639
+ true
640
+ );
641
+ await this.setForeignStateAsync(`${yesterdayBase}.routeRankingText`, this.topRankingText(routeSorted, 5), true);
642
+ const historyId = `${base}.history.daily`;
643
+ const history = await this.readJsonState(historyId, []);
644
+ const entry = {
645
+ date: storedDate,
646
+ totalFlights,
647
+ landings,
648
+ departures,
649
+ overflights,
650
+ bestHour: bestHour.hour,
651
+ bestHourFlights: bestHour.total,
652
+ topAirline,
653
+ topRoute,
654
+ a380Count,
655
+ specialLiveryCount
656
+ };
657
+ const limitedHistory = [entry, ...history.filter((item) => this.clean(item == null ? void 0 : item.date) !== storedDate)].slice(0, 365);
658
+ const historyText = limitedHistory.slice(0, 14).map((item) => {
659
+ const best = item.bestHour ? ` \xB7 beste Zeit ${item.bestHour}` : "";
660
+ const special = Number(item.specialLiveryCount || 0) > 0 ? ` \xB7 \u2B50 ${item.specialLiveryCount}` : "";
661
+ const heavy = Number(item.heavyAircraftCount || 0) > 0 ? ` \xB7 Heavy ${item.heavyAircraftCount}` : "";
662
+ const a380 = Number(item.a380Count || 0) > 0 ? ` \xB7 A380 ${item.a380Count}` : "";
663
+ const b747 = Number(item.b747Count || 0) > 0 ? ` \xB7 B747 ${item.b747Count}` : "";
664
+ return `${item.date}: ${item.totalFlights} Fl\xFCge${best}${heavy}${a380}${b747}${special}`;
665
+ }).join("\n");
666
+ await this.setForeignStateAsync(historyId, JSON.stringify(limitedHistory), true);
667
+ await this.setForeignStateAsync(`${base}.history.dailyText`, historyText, true);
668
+ const currentBestDayFlights = await this.readNumberState(`${base}.alltime.bestDayFlights`);
669
+ if (totalFlights > currentBestDayFlights) {
670
+ await this.setForeignStateAsync(`${base}.alltime.bestDayDate`, storedDate, true);
671
+ await this.setForeignStateAsync(`${base}.alltime.bestDayFlights`, totalFlights, true);
672
+ }
673
+ const currentBestSpecial = await this.readNumberState(`${base}.alltime.bestSpecialDayCount`);
674
+ if (specialLiveryCount > currentBestSpecial) {
675
+ await this.setForeignStateAsync(`${base}.alltime.bestSpecialDayDate`, storedDate, true);
676
+ await this.setForeignStateAsync(`${base}.alltime.bestSpecialDayCount`, specialLiveryCount, true);
677
+ }
678
+ await this.setForeignStateAsync(
679
+ `${base}.alltime.a380Count`,
680
+ await this.readNumberState(`${base}.alltime.a380Count`) + a380Count,
681
+ true
682
+ );
683
+ await this.setForeignStateAsync(
684
+ `${base}.alltime.specialLiveryCount`,
685
+ await this.readNumberState(`${base}.alltime.specialLiveryCount`) + specialLiveryCount,
686
+ true
687
+ );
688
+ const alltimeHourly = await this.readJsonState(`${base}.alltime.hourly`, {});
689
+ for (const [hour, value] of Object.entries(hourly || {})) {
690
+ alltimeHourly[hour] = (alltimeHourly[hour] || 0) + Number((value == null ? void 0 : value.total) || 0);
691
+ }
692
+ const bestAlltimeHour = Object.entries(alltimeHourly).filter(([, count]) => Number(count || 0) > 0).sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0) || Number(a[0]) - Number(b[0]))[0];
693
+ await this.setForeignStateAsync(`${base}.alltime.hourly`, JSON.stringify(alltimeHourly), true);
694
+ await this.setForeignStateAsync(
695
+ `${base}.alltime.bestHour`,
696
+ bestAlltimeHour ? `${bestAlltimeHour[0]}:00` : "",
697
+ true
698
+ );
699
+ await this.setForeignStateAsync(
700
+ `${base}.alltime.bestHourFlights`,
701
+ bestAlltimeHour ? Number(bestAlltimeHour[1] || 0) : 0,
702
+ true
703
+ );
704
+ const alltimeAirlines = this.mergeRanking(
705
+ await this.readJsonState(`${base}.alltime.airlineRanking`, {}),
706
+ airlineRanking
707
+ );
708
+ const alltimeRoutes = this.mergeRanking(
709
+ await this.readJsonState(`${base}.alltime.routeRanking`, {}),
710
+ routeRanking
711
+ );
712
+ const alltimeAirlineSorted = this.sortedRanking(alltimeAirlines, 50);
713
+ const alltimeRouteSorted = this.sortedRanking(alltimeRoutes, 50);
714
+ await this.setForeignStateAsync(
715
+ `${base}.alltime.airlineRanking`,
716
+ JSON.stringify(Object.fromEntries(alltimeAirlineSorted)),
717
+ true
718
+ );
719
+ await this.setForeignStateAsync(
720
+ `${base}.alltime.airlineRankingText`,
721
+ this.topRankingText(alltimeAirlineSorted, 8),
722
+ true
723
+ );
724
+ await this.setForeignStateAsync(
725
+ `${base}.alltime.routeRanking`,
726
+ JSON.stringify(Object.fromEntries(alltimeRouteSorted)),
727
+ true
728
+ );
729
+ await this.setForeignStateAsync(
730
+ `${base}.alltime.routeRankingText`,
731
+ this.topRankingText(alltimeRouteSorted, 8),
732
+ true
733
+ );
734
+ await this.setForeignStateAsync(
735
+ `${base}.alltime.bestAirline`,
736
+ alltimeAirlineSorted.length ? `${alltimeAirlineSorted[0][0]} \xB7 ${alltimeAirlineSorted[0][1]}` : "",
737
+ true
738
+ );
739
+ await this.setForeignStateAsync(
740
+ `${base}.alltime.bestRoute`,
741
+ alltimeRouteSorted.length ? `${alltimeRouteSorted[0][0]} \xB7 ${alltimeRouteSorted[0][1]}` : "",
742
+ true
743
+ );
744
+ const aircraftTypeRanking = await this.readJsonState(`${base}.aircraftTypeRanking`, {});
745
+ const aircraftTypeSorted = this.sortedRanking(aircraftTypeRanking, 50);
746
+ await this.setForeignStateAsync(
747
+ `${base}.alltime.bestAircraftType`,
748
+ aircraftTypeSorted.length ? `${aircraftTypeSorted[0][0]} \xB7 ${aircraftTypeSorted[0][1]}` : "",
749
+ true
750
+ );
751
+ this.log.info(`[JetFrame] Tagesstatistik archiviert: ${storedDate} \xB7 ${totalFlights} Fl\xFCge`);
752
+ }
753
+ todayDateKey() {
754
+ const d = /* @__PURE__ */ new Date();
755
+ const yyyy = d.getFullYear();
756
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
757
+ const dd = String(d.getDate()).padStart(2, "0");
758
+ return `${yyyy}-${mm}-${dd}`;
759
+ }
760
+ async resetTodayStatisticsIfNeeded(base) {
761
+ const todayBase = `${base}.today`;
762
+ const today = this.todayDateKey();
763
+ let storedDate = "";
764
+ try {
765
+ const st = await this.getForeignStateAsync(`${todayBase}.date`);
766
+ storedDate = this.clean(st == null ? void 0 : st.val);
767
+ } catch {
768
+ storedDate = "";
769
+ }
770
+ if (storedDate === today) {
771
+ return;
772
+ }
773
+ await this.archiveTodayStatistics(base, storedDate);
774
+ await this.setForeignStateAsync(`${todayBase}.date`, today, true);
775
+ await this.setForeignStateAsync(`${todayBase}.totalFlights`, 0, true);
776
+ await this.setForeignStateAsync(`${todayBase}.landings`, 0, true);
777
+ await this.setForeignStateAsync(`${todayBase}.departures`, 0, true);
778
+ await this.setForeignStateAsync(`${todayBase}.overflights`, 0, true);
779
+ await this.setForeignStateAsync(`${todayBase}.firstSeen`, "", true);
780
+ await this.setForeignStateAsync(`${todayBase}.lastSeen`, "", true);
781
+ await this.setForeignStateAsync(`${todayBase}.lastFlight`, "", true);
782
+ await this.setForeignStateAsync(`${todayBase}.lastAirline`, "", true);
783
+ await this.setForeignStateAsync(`${todayBase}.lastRoute`, "", true);
784
+ await this.setForeignStateAsync(`${todayBase}.lastRegistration`, "", true);
785
+ await this.setForeignStateAsync(`${todayBase}.topAirline`, "", true);
786
+ await this.setForeignStateAsync(`${todayBase}.topRoute`, "", true);
787
+ await this.setForeignStateAsync(`${todayBase}.airlineRanking`, "{}", true);
788
+ await this.setForeignStateAsync(`${todayBase}.airlineRankingText`, "", true);
789
+ await this.setForeignStateAsync(`${todayBase}.routeRanking`, "{}", true);
790
+ await this.setForeignStateAsync(`${todayBase}.routeRankingText`, "", true);
791
+ await this.setForeignStateAsync(`${todayBase}.hourly`, JSON.stringify(this.emptyHourlyStats()), true);
792
+ await this.setForeignStateAsync(`${todayBase}.hourlyText`, "", true);
793
+ await this.setForeignStateAsync(`${todayBase}.bestSpotterHour`, "", true);
794
+ await this.setForeignStateAsync(`${todayBase}.currentHourFlights`, 0, true);
795
+ await this.setForeignStateAsync(`${todayBase}.rushHourNow`, false, true);
796
+ await this.setForeignStateAsync(`${todayBase}.rushHourText`, "", true);
797
+ await this.setForeignStateAsync(`${todayBase}.a380Count`, 0, true);
798
+ await this.setForeignStateAsync(`${todayBase}.b747Count`, 0, true);
799
+ await this.setForeignStateAsync(`${todayBase}.heavyAircraftCount`, 0, true);
800
+ await this.setForeignStateAsync(`${todayBase}.specialLiveryCount`, 0, true);
801
+ }
802
+ emptyHourlyStats() {
803
+ const result = {};
804
+ for (let h = 0; h < 24; h++) {
805
+ const key = String(h).padStart(2, "0");
806
+ result[key] = {
807
+ total: 0,
808
+ landings: 0,
809
+ departures: 0,
810
+ overflights: 0
811
+ };
812
+ }
813
+ return result;
814
+ }
815
+ async updateTodayHourlyStatistics(todayBase, mode) {
816
+ var _a;
817
+ const now = /* @__PURE__ */ new Date();
818
+ const hour = String(now.getHours()).padStart(2, "0");
819
+ const hourly = await this.readJsonState(`${todayBase}.hourly`, this.emptyHourlyStats());
820
+ if (!hourly[hour]) {
821
+ hourly[hour] = {
822
+ total: 0,
823
+ landings: 0,
824
+ departures: 0,
825
+ overflights: 0
826
+ };
827
+ }
828
+ hourly[hour].total += 1;
829
+ if (mode === "LANDING") {
830
+ hourly[hour].landings += 1;
831
+ } else if (mode === "TAKEOFF") {
832
+ hourly[hour].departures += 1;
833
+ } else if (mode === "OVERFLIGHT") {
834
+ hourly[hour].overflights += 1;
835
+ }
836
+ const entries = Object.entries(hourly).sort((a, b) => Number(a[0]) - Number(b[0]));
837
+ const activeEntries = entries.filter(([, v]) => v.total > 0);
838
+ const hourlyText = activeEntries.map(([h, v]) => `${h}:00 \xB7 ${v.total} (${v.landings}\u{1F6EC} ${v.departures}\u{1F6EB} ${v.overflights}\u{1F6E9}\uFE0F)`).join("\n");
839
+ const best = activeEntries.slice().sort((a, b) => b[1].total - a[1].total || Number(a[0]) - Number(b[0]))[0];
840
+ const currentHourTotal = ((_a = hourly[hour]) == null ? void 0 : _a.total) || 0;
841
+ const avgActive = activeEntries.length > 0 ? activeEntries.reduce((sum, [, v]) => sum + v.total, 0) / activeEntries.length : 0;
842
+ const rushHourNow = currentHourTotal >= 5 && currentHourTotal >= Math.max(3, Math.ceil(avgActive * 1.35));
843
+ const rushHourText = rushHourNow ? `\u{1F525} Rushhour: ${currentHourTotal} Fl\xFCge seit ${hour}:00` : currentHourTotal > 0 ? `Aktuelle Stunde: ${currentHourTotal} Fl\xFCge` : "";
844
+ await this.setForeignStateAsync(`${todayBase}.hourly`, JSON.stringify(hourly), true);
845
+ await this.setForeignStateAsync(`${todayBase}.hourlyText`, hourlyText, true);
846
+ await this.setForeignStateAsync(
847
+ `${todayBase}.bestSpotterHour`,
848
+ best ? `${best[0]}:00 \xB7 ${best[1].total} Fl\xFCge` : "",
849
+ true
850
+ );
851
+ await this.setForeignStateAsync(`${todayBase}.currentHourFlights`, currentHourTotal, true);
852
+ await this.setForeignStateAsync(`${todayBase}.rushHourNow`, rushHourNow, true);
853
+ await this.setForeignStateAsync(`${todayBase}.rushHourText`, rushHourText, true);
854
+ }
855
+ async updateTodayStatistics(base, a, info) {
856
+ var _a;
857
+ await this.resetTodayStatisticsIfNeeded(base);
858
+ const todayBase = `${base}.today`;
859
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
860
+ const total = await this.readNumberState(`${todayBase}.totalFlights`) + 1;
861
+ await this.setForeignStateAsync(`${todayBase}.totalFlights`, total, true);
862
+ if (info.mode === "LANDING") {
863
+ await this.setForeignStateAsync(
864
+ `${todayBase}.landings`,
865
+ await this.readNumberState(`${todayBase}.landings`) + 1,
866
+ true
867
+ );
868
+ } else if (info.mode === "TAKEOFF") {
869
+ await this.setForeignStateAsync(
870
+ `${todayBase}.departures`,
871
+ await this.readNumberState(`${todayBase}.departures`) + 1,
872
+ true
873
+ );
874
+ } else if (info.mode === "OVERFLIGHT") {
875
+ await this.setForeignStateAsync(
876
+ `${todayBase}.overflights`,
877
+ await this.readNumberState(`${todayBase}.overflights`) + 1,
878
+ true
879
+ );
880
+ }
881
+ try {
882
+ const firstSeen = this.clean((_a = await this.getForeignStateAsync(`${todayBase}.firstSeen`)) == null ? void 0 : _a.val);
883
+ if (!firstSeen) {
884
+ await this.setForeignStateAsync(`${todayBase}.firstSeen`, nowIso, true);
885
+ }
886
+ } catch {
887
+ await this.setForeignStateAsync(`${todayBase}.firstSeen`, nowIso, true);
888
+ }
889
+ await this.setForeignStateAsync(`${todayBase}.lastSeen`, nowIso, true);
890
+ await this.setForeignStateAsync(`${todayBase}.lastFlight`, info.callsign, true);
891
+ await this.setForeignStateAsync(`${todayBase}.lastAirline`, info.airline, true);
892
+ await this.setForeignStateAsync(`${todayBase}.lastRoute`, info.route, true);
893
+ await this.setForeignStateAsync(`${todayBase}.lastRegistration`, info.registration, true);
894
+ const airlineRanking = await this.readJsonState(`${todayBase}.airlineRanking`, {});
895
+ airlineRanking[info.airline] = (airlineRanking[info.airline] || 0) + 1;
896
+ const airlineSorted = Object.entries(airlineRanking).sort((aEntry, bEntry) => bEntry[1] - aEntry[1] || aEntry[0].localeCompare(bEntry[0])).slice(0, 20);
897
+ const airlineText = airlineSorted.slice(0, 5).map(([name, count], index) => `${index + 1}. ${name} \xB7 ${count}`).join("\n");
898
+ await this.setForeignStateAsync(
899
+ `${todayBase}.airlineRanking`,
900
+ JSON.stringify(Object.fromEntries(airlineSorted)),
901
+ true
902
+ );
903
+ await this.setForeignStateAsync(`${todayBase}.airlineRankingText`, airlineText, true);
904
+ await this.setForeignStateAsync(
905
+ `${todayBase}.topAirline`,
906
+ airlineSorted.length ? `${airlineSorted[0][0]} \xB7 ${airlineSorted[0][1]}` : "",
907
+ true
908
+ );
909
+ if (info.route) {
910
+ const routeRanking = await this.readJsonState(`${todayBase}.routeRanking`, {});
911
+ routeRanking[info.route] = (routeRanking[info.route] || 0) + 1;
912
+ const routeSorted = Object.entries(routeRanking).filter(([routeName]) => {
913
+ const r = this.clean(routeName);
914
+ return r && !r.includes("?") && r.includes("\u2192");
915
+ }).sort((aEntry, bEntry) => bEntry[1] - aEntry[1] || aEntry[0].localeCompare(bEntry[0])).slice(0, 20);
916
+ const routeText = routeSorted.slice(0, 5).map(([name, count], index) => `${index + 1}. ${name} \xB7 ${count}`).join("\n");
917
+ await this.setForeignStateAsync(
918
+ `${todayBase}.routeRanking`,
919
+ JSON.stringify(Object.fromEntries(routeSorted)),
920
+ true
921
+ );
922
+ await this.setForeignStateAsync(`${todayBase}.routeRankingText`, routeText, true);
923
+ await this.setForeignStateAsync(
924
+ `${todayBase}.topRoute`,
925
+ routeSorted.length ? `${routeSorted[0][0]} \xB7 ${routeSorted[0][1]}` : "",
926
+ true
927
+ );
928
+ }
929
+ if (this.isA380(a)) {
930
+ await this.setForeignStateAsync(
931
+ `${todayBase}.a380Count`,
932
+ await this.readNumberState(`${todayBase}.a380Count`) + 1,
933
+ true
934
+ );
935
+ }
936
+ if (this.isB747(a)) {
937
+ await this.setForeignStateAsync(
938
+ `${todayBase}.b747Count`,
939
+ await this.readNumberState(`${todayBase}.b747Count`) + 1,
940
+ true
941
+ );
942
+ }
943
+ if (this.isHeavyAircraft(a)) {
944
+ await this.setForeignStateAsync(
945
+ `${todayBase}.heavyAircraftCount`,
946
+ await this.readNumberState(`${todayBase}.heavyAircraftCount`) + 1,
947
+ true
948
+ );
949
+ }
950
+ if (this.isSpecialFlight(a)) {
951
+ await this.setForeignStateAsync(
952
+ `${todayBase}.specialLiveryCount`,
953
+ await this.readNumberState(`${todayBase}.specialLiveryCount`) + 1,
954
+ true
955
+ );
956
+ }
957
+ await this.updateTodayHourlyStatistics(todayBase, info.mode);
958
+ }
959
+ async updateStatistics(dpRoot, a) {
960
+ const base = `${dpRoot}.statistics`;
961
+ const mode = String(a.mode || "").toUpperCase();
962
+ const callsign = this.clean(a.routeCallsign || a.callsign || a.hex || "");
963
+ const airline = this.clean(a.airlineName || "Unbekannte Airline");
964
+ const registration = this.clean(a.registration || "");
965
+ const origin = this.clean(a.originIata || "");
966
+ const dest = this.clean(a.destIata || "");
967
+ const route = origin && dest ? `${origin} \u2192 ${dest}` : "";
968
+ await this.setForeignStateAsync(
969
+ `${base}.totalFlightsSeen`,
970
+ await this.readNumberState(`${base}.totalFlightsSeen`) + 1,
971
+ true
972
+ );
973
+ if (mode === "LANDING") {
974
+ await this.setForeignStateAsync(
975
+ `${base}.landings`,
976
+ await this.readNumberState(`${base}.landings`) + 1,
977
+ true
978
+ );
979
+ } else if (mode === "TAKEOFF") {
980
+ await this.setForeignStateAsync(
981
+ `${base}.departures`,
982
+ await this.readNumberState(`${base}.departures`) + 1,
983
+ true
984
+ );
985
+ } else if (mode === "OVERFLIGHT") {
986
+ await this.setForeignStateAsync(
987
+ `${base}.overflights`,
988
+ await this.readNumberState(`${base}.overflights`) + 1,
989
+ true
990
+ );
991
+ }
992
+ await this.setForeignStateAsync(`${base}.lastFlight`, callsign, true);
993
+ await this.setForeignStateAsync(`${base}.lastAirline`, airline, true);
994
+ await this.setForeignStateAsync(`${base}.lastRoute`, route, true);
995
+ await this.setForeignStateAsync(`${base}.lastRegistration`, registration, true);
996
+ await this.setForeignStateAsync(`${base}.lastSeen`, (/* @__PURE__ */ new Date()).toISOString(), true);
997
+ const ranking = await this.readJsonState(`${base}.airlineRanking`, {});
998
+ ranking[airline] = (ranking[airline] || 0) + 1;
999
+ const sorted = Object.entries(ranking).sort((aEntry, bEntry) => bEntry[1] - aEntry[1] || aEntry[0].localeCompare(bEntry[0])).slice(0, 20);
1000
+ const rankingJson = Object.fromEntries(sorted);
1001
+ const rankingText = sorted.slice(0, 5).map(([name, count], index) => `${index + 1}. ${name} \xB7 ${count}`).join("\n");
1002
+ await this.setForeignStateAsync(`${base}.airlineRanking`, JSON.stringify(rankingJson), true);
1003
+ await this.setForeignStateAsync(`${base}.airlineRankingText`, rankingText, true);
1004
+ await this.updateGlobalDetailedStatistics(base, a, {
1005
+ mode,
1006
+ callsign,
1007
+ airline,
1008
+ registration,
1009
+ route
1010
+ });
1011
+ await this.updateTodayStatistics(base, a, {
1012
+ mode,
1013
+ callsign,
1014
+ airline,
1015
+ registration,
1016
+ route
1017
+ });
1018
+ }
1019
+ async loop() {
1020
+ try {
1021
+ this.clearTimer();
1022
+ const config = (0, import_config.readConfig)(this);
1023
+ if (!config.enabled) {
1024
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "disabled", true);
1025
+ this.scheduleNext(config.searchPollSeconds);
1026
+ return;
1027
+ }
1028
+ if (this.liveTarget) {
1029
+ await this.liveLoop();
1030
+ } else {
1031
+ await this.searchLoop();
1032
+ }
1033
+ } catch (e) {
1034
+ this.logError(`JetFrame Fehler: ${this.errorText(e)}`);
1035
+ const config = (0, import_config.readConfig)(this);
1036
+ this.scheduleNext(config.searchPollSeconds);
1037
+ }
1038
+ }
1039
+ async searchLoop() {
1040
+ const config = (0, import_config.readConfig)(this);
1041
+ this.log.debug("Search Loop gestartet");
1042
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "searching", true);
1043
+ const data = await (0, import_adsb.fetchAdsb)(
1044
+ config,
1045
+ this.httpJsonRaw.bind(this),
1046
+ this.logWarn.bind(this),
1047
+ this.logDebug.bind(this)
1048
+ );
1049
+ this.log.debug("[JetFrame] ADSB Fetch OK");
1050
+ const aircraft = (0, import_adsb.parseAircraft)(data);
1051
+ this.log.debug(`[JetFrame] ADSB parsed: ${aircraft.length}`);
1052
+ this.log.debug(`Aircraft parsed: ${aircraft.length}`);
1053
+ const matches = (0, import_classify.getMatches)(config, aircraft);
1054
+ if (matches.length) {
1055
+ await this.updateIdleRunway(matches[0], config);
1056
+ }
1057
+ this.log.debug(`Matches gefunden: ${matches.length}`);
1058
+ await this.setForeignStateAsync(`${config.dpRoot}.lastUpdate`, (/* @__PURE__ */ new Date()).toISOString(), true);
1059
+ await this.setForeignStateAsync(`${config.dpRoot}.allCount`, aircraft.length, true);
1060
+ await this.setForeignStateAsync(`${config.dpRoot}.matchCount`, matches.length, true);
1061
+ if (!matches.length) {
1062
+ await this.setForeignStateAsync(
1063
+ `${config.dpRoot}.current.text`,
1064
+ `Kein Start/Landung/\xDCberflug bei ${config.airport.iata}`,
1065
+ true
1066
+ );
1067
+ this.scheduleNext(config.searchPollSeconds);
1068
+ return;
1069
+ }
1070
+ this.log.debug(
1071
+ `Best Match: ${matches[0].callsign || matches[0].hex || "?"} | alt=${matches[0].altFt}ft | mode=${matches[0].mode}`
1072
+ );
1073
+ await this.startNewFlight(matches[0]);
1074
+ }
1075
+ async liveLoop() {
1076
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
1077
+ const config = (0, import_config.readConfig)(this);
1078
+ const elapsed = (Date.now() - this.liveStarted) / 1e3;
1079
+ const data = await (0, import_adsb.fetchAdsb)(
1080
+ config,
1081
+ this.httpJsonRaw.bind(this),
1082
+ this.logWarn.bind(this),
1083
+ this.logDebug.bind(this)
1084
+ );
1085
+ const aircraft = (0, import_adsb.parseAircraft)(data);
1086
+ const matches = (0, import_classify.getMatches)(config, aircraft);
1087
+ await this.setForeignStateAsync(`${config.dpRoot}.lastUpdate`, (/* @__PURE__ */ new Date()).toISOString(), true);
1088
+ await this.setForeignStateAsync(`${config.dpRoot}.allCount`, aircraft.length, true);
1089
+ await this.setForeignStateAsync(`${config.dpRoot}.matchCount`, matches.length, true);
1090
+ const live = this.findCurrentLive(matches, this.liveTarget);
1091
+ const bestNow = matches[0];
1092
+ if (bestNow && !live && this.isDifferentAircraft(bestNow, this.liveTarget)) {
1093
+ this.log.info(`Neues Flugzeug erkannt, schalte um: ${bestNow.callsign || bestNow.hex}`);
1094
+ await this.startNewFlight(bestNow);
1095
+ return;
1096
+ }
1097
+ if (elapsed >= config.liveMaxSeconds) {
1098
+ this.liveTarget = null;
1099
+ this.liveStarted = 0;
1100
+ this.liveInfo = null;
1101
+ await (0, import_states.clearFlight)(this, `${config.dpRoot}.current`);
1102
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "cleared", true);
1103
+ this.scheduleNext(config.searchPollSeconds);
1104
+ return;
1105
+ }
1106
+ if (!live) {
1107
+ this.liveTarget = null;
1108
+ this.liveStarted = 0;
1109
+ this.log.info("Live Flug verloren");
1110
+ await (0, import_states.clearFlight)(this, `${config.dpRoot}.current`);
1111
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "lost", true);
1112
+ this.scheduleNext(config.searchPollSeconds);
1113
+ return;
1114
+ }
1115
+ const bases = [`${config.dpRoot}.current`];
1116
+ if (((_a = this.liveTarget) == null ? void 0 : _a.mode) === "OVERFLIGHT") {
1117
+ bases.push(`${config.dpRoot}.overflight`);
1118
+ } else {
1119
+ bases.push(`${config.dpRoot}.airport`);
1120
+ }
1121
+ const enrichedLive = {
1122
+ ...this.liveInfo || {},
1123
+ ...live,
1124
+ // Diese Werte kommen nur aus saveImages()/enrichFlightInfo
1125
+ // und dürfen vom Live-ADS-B-Update nicht wieder leer überschrieben werden.
1126
+ localLogoUrl: ((_b = this.liveInfo) == null ? void 0 : _b.localLogoUrl) || live.localLogoUrl || "",
1127
+ localImageUrl: ((_c = this.liveInfo) == null ? void 0 : _c.localImageUrl) || live.localImageUrl || "",
1128
+ finalImageUrl: ((_d = this.liveInfo) == null ? void 0 : _d.finalImageUrl) || live.finalImageUrl || "",
1129
+ logoUrl: ((_e = this.liveInfo) == null ? void 0 : _e.logoUrl) || live.logoUrl || "",
1130
+ routeCallsign: ((_f = this.liveInfo) == null ? void 0 : _f.routeCallsign) || live.routeCallsign || live.callsign || "",
1131
+ aircraftModel: ((_g = this.liveInfo) == null ? void 0 : _g.aircraftModel) || live.aircraftModel || live.aircraftType || live.type || "",
1132
+ isSpecial: ((_h = this.liveInfo) == null ? void 0 : _h.isSpecial) || live.isSpecial || false,
1133
+ specialText: ((_i = this.liveInfo) == null ? void 0 : _i.specialText) || live.specialText || "",
1134
+ specialLiveryTitle: ((_j = this.liveInfo) == null ? void 0 : _j.specialLiveryTitle) || live.specialLiveryTitle || "",
1135
+ specialLiveryDescription: ((_k = this.liveInfo) == null ? void 0 : _k.specialLiveryDescription) || live.specialLiveryDescription || "",
1136
+ specialLiveryFull: ((_l = this.liveInfo) == null ? void 0 : _l.specialLiveryFull) || live.specialLiveryFull || "",
1137
+ specialLiveryVisText: ((_m = this.liveInfo) == null ? void 0 : _m.specialLiveryVisText) || live.specialLiveryVisText || ""
1138
+ };
1139
+ await this.applyProbableRunway(enrichedLive, config);
1140
+ this.liveInfo = {
1141
+ ...enrichedLive
1142
+ };
1143
+ for (const base of bases) {
1144
+ await (0, import_states.writeFlight)(this, base, enrichedLive);
1145
+ }
1146
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "live", true);
1147
+ this.scheduleNext(config.livePollSeconds);
1148
+ }
1149
+ async applySpecialLivery(a, dpRoot) {
1150
+ const reg = this.clean(a.registration).toUpperCase();
1151
+ if (!reg) {
1152
+ return;
1153
+ }
1154
+ try {
1155
+ const st = await this.getForeignStateAsync(`${dpRoot}.specialLiveries`);
1156
+ const raw = String((st == null ? void 0 : st.val) || "").trim();
1157
+ if (!raw || raw === "[]") {
1158
+ return;
1159
+ }
1160
+ const list = JSON.parse(raw);
1161
+ if (!Array.isArray(list)) {
1162
+ return;
1163
+ }
1164
+ const hit = list.find((item) => {
1165
+ const itemReg = this.clean(item == null ? void 0 : item.registration).toUpperCase();
1166
+ return itemReg && itemReg === reg;
1167
+ });
1168
+ if (!hit) {
1169
+ return;
1170
+ }
1171
+ const emoji = this.clean(hit.emoji) || "\u{1F3A8}";
1172
+ const title = this.clean(hit.title) || this.clean(hit.type) || "Special Livery";
1173
+ const description = this.clean(hit.description);
1174
+ const airline = this.clean(hit.airline);
1175
+ a.isSpecial = true;
1176
+ a.specialLiveryTitle = title;
1177
+ a.specialLiveryDescription = description;
1178
+ a.specialLiveryFull = description || title;
1179
+ a.specialLiveryVisText = `${emoji} ${title}`;
1180
+ a.specialText = `${emoji} ${title}${airline ? ` \xB7 ${airline}` : ""}`;
1181
+ this.log.info(`Special Livery erkannt: ${reg} \xB7 ${title}`);
1182
+ } catch (e) {
1183
+ this.logWarn(`Special-Livery Match Fehler: ${this.errorText(e)}`);
1184
+ }
1185
+ }
1186
+ async startNewFlight(rawMatch) {
1187
+ const config = (0, import_config.readConfig)(this);
1188
+ const startKey = this.flightKey(rawMatch);
1189
+ const now = Date.now();
1190
+ if (startKey && startKey === this.lastStartKey && now - this.lastStartTs < 15e3) {
1191
+ this.log.debug(`Gleicher Flug wurde gerade erst gestartet \u2192 ignoriere: ${startKey}`);
1192
+ this.scheduleNext(config.livePollSeconds);
1193
+ return;
1194
+ }
1195
+ this.lastStartKey = startKey;
1196
+ this.lastStartTs = now;
1197
+ this.log.debug(`Starte neuen Flug: ${rawMatch.callsign || rawMatch.hex}`);
1198
+ const best = await (0, import_flightInfo.enrichFlightInfo)(
1199
+ this,
1200
+ config,
1201
+ rawMatch,
1202
+ this.httpJson.bind(this),
1203
+ this.httpText.bind(this),
1204
+ this.logDebug.bind(this),
1205
+ this.logWarn.bind(this)
1206
+ );
1207
+ await this.applySpecialLivery(best, config.dpRoot);
1208
+ await this.applyProbableRunway(best, config);
1209
+ this.log.info(
1210
+ `Neuer Flug: callsign=${best.callsign || ""} route=${best.originIata || "?"} \u2192 ${best.destIata || "?"} | ${best.originName || "?"} \u2192 ${best.destName || "?"}${best.probableRunwayText ? ` | ${best.probableRunwayText}` : ""}`
1211
+ );
1212
+ await this.updateStatistics(config.dpRoot, best);
1213
+ this.liveTarget = {
1214
+ hex: best.hex,
1215
+ callsign: best.callsign,
1216
+ mode: best.mode
1217
+ };
1218
+ const imageSaveKey = this.flightKey(best);
1219
+ if (imageSaveKey && imageSaveKey !== this.lastImageSaveKey) {
1220
+ await (0, import_images.saveImages)(this, config, best, this.logDebug.bind(this), this.logWarn.bind(this));
1221
+ this.lastImageSaveKey = imageSaveKey;
1222
+ }
1223
+ this.liveInfo = {
1224
+ ...best
1225
+ };
1226
+ this.liveStarted = Date.now();
1227
+ await (0, import_states.writeFlight)(this, `${config.dpRoot}.current`, best);
1228
+ if (best.mode === "OVERFLIGHT") {
1229
+ await (0, import_states.writeFlight)(this, `${config.dpRoot}.overflight`, best);
1230
+ } else {
1231
+ await (0, import_states.writeFlight)(this, `${config.dpRoot}.airport`, best);
1232
+ }
1233
+ await this.setForeignStateAsync(`${config.dpRoot}.status`, "live", true);
1234
+ this.scheduleNext(config.livePollSeconds);
1235
+ }
1236
+ findCurrentLive(matches, target) {
1237
+ if (!matches.length || !target) {
1238
+ return null;
1239
+ }
1240
+ return matches.find((a) => {
1241
+ const aHex = this.clean(a.hex).toLowerCase();
1242
+ const tHex = this.clean(target.hex).toLowerCase();
1243
+ const aCall = this.clean(a.callsign).toUpperCase();
1244
+ const tCall = this.clean(target.callsign).toUpperCase();
1245
+ if (aHex && tHex && aHex === tHex) {
1246
+ return true;
1247
+ }
1248
+ if (aCall && tCall && aCall === tCall) {
1249
+ return true;
1250
+ }
1251
+ return false;
1252
+ }) || null;
1253
+ }
1254
+ isDifferentAircraft(a, target) {
1255
+ if (!a || !target) {
1256
+ return false;
1257
+ }
1258
+ const aHex = this.clean(a.hex).toLowerCase();
1259
+ const tHex = this.clean(target.hex).toLowerCase();
1260
+ const aCall = this.clean(a.callsign).toUpperCase();
1261
+ const tCall = this.clean(target.callsign).toUpperCase();
1262
+ if (aCall && tCall && aCall === tCall) {
1263
+ return false;
1264
+ }
1265
+ if (aHex && tHex && aHex === tHex) {
1266
+ return false;
1267
+ }
1268
+ if (aCall && tCall) {
1269
+ return aCall !== tCall;
1270
+ }
1271
+ if (aHex && tHex) {
1272
+ return aHex !== tHex;
1273
+ }
1274
+ return false;
1275
+ }
1276
+ flightKey(a) {
1277
+ const hex = this.clean(a.hex).toLowerCase();
1278
+ const cs = this.clean(a.callsign).toUpperCase();
1279
+ if (cs) {
1280
+ return `CS:${cs}`;
1281
+ }
1282
+ if (hex) {
1283
+ return `HEX:${hex}`;
1284
+ }
1285
+ return "";
1286
+ }
1287
+ scheduleStatisticsRotation(dpRoot) {
1288
+ if (this.statisticsTimer) {
1289
+ clearInterval(this.statisticsTimer);
1290
+ this.statisticsTimer = null;
1291
+ }
1292
+ this.statisticsTimer = setInterval(() => {
1293
+ this.resetTodayStatisticsIfNeeded(`${dpRoot}.statistics`).catch((e) => {
1294
+ this.logWarn(`Tagesstatistik-Rotation fehlgeschlagen: ${this.errorText(e)}`);
1295
+ });
1296
+ }, 6e4);
1297
+ }
1298
+ clearStatisticsTimer() {
1299
+ if (this.statisticsTimer) {
1300
+ clearInterval(this.statisticsTimer);
1301
+ this.statisticsTimer = null;
1302
+ }
1303
+ }
1304
+ scheduleNext(seconds) {
1305
+ this.timer = setTimeout(() => this.loop(), seconds * 1e3);
1306
+ }
1307
+ clearTimer() {
1308
+ if (this.timer) {
1309
+ clearTimeout(this.timer);
1310
+ this.timer = null;
1311
+ }
1312
+ }
1313
+ async httpJson(url) {
1314
+ const res = await this.httpRequest(url, {
1315
+ timeout: 12e3,
1316
+ headers: {
1317
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
1318
+ Accept: "application/json,text/plain,*/*",
1319
+ "Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
1320
+ "Cache-Control": "no-cache",
1321
+ Pragma: "no-cache"
1322
+ }
1323
+ });
1324
+ if (typeof res === "string") {
1325
+ return JSON.parse(res);
1326
+ }
1327
+ return res;
1328
+ }
1329
+ async httpJsonRaw(url) {
1330
+ const res = await this.httpRequest(url, {
1331
+ timeout: 2e4,
1332
+ headers: {
1333
+ "User-Agent": "Mozilla/5.0",
1334
+ Accept: "application/json,text/plain,*/*"
1335
+ }
1336
+ });
1337
+ if (typeof res === "string") {
1338
+ const text = res.trim();
1339
+ if (text.startsWith("<")) {
1340
+ throw new Error("HTML statt JSON erhalten");
1341
+ }
1342
+ return JSON.parse(text);
1343
+ }
1344
+ return res;
1345
+ }
1346
+ async httpText(url) {
1347
+ const res = await this.httpRequest(url, {
1348
+ timeout: 15e3,
1349
+ headers: {
1350
+ "User-Agent": "Mozilla/5.0 AppleWebKit/605.1.15 Safari/604.1",
1351
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1352
+ "Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
1353
+ Referer: "https://www.google.com/"
1354
+ }
1355
+ });
1356
+ return String(res || "");
1357
+ }
1358
+ httpRequest(url, options = {}) {
1359
+ return new Promise((resolve, reject) => {
1360
+ const client = url.startsWith("https") ? https : http;
1361
+ const req = client.get(
1362
+ url,
1363
+ {
1364
+ headers: options.headers || {},
1365
+ timeout: options.timeout || 15e3
1366
+ },
1367
+ (res) => {
1368
+ let body = "";
1369
+ res.on("data", (chunk) => {
1370
+ body += chunk;
1371
+ });
1372
+ res.on("end", () => {
1373
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
1374
+ this.httpRequest(res.headers.location, options).then(resolve).catch(reject);
1375
+ return;
1376
+ }
1377
+ if (res.statusCode && res.statusCode >= 400) {
1378
+ reject(new Error(`HTTP ${res.statusCode} bei ${url}`));
1379
+ return;
1380
+ }
1381
+ resolve(body);
1382
+ });
1383
+ }
1384
+ );
1385
+ req.on("error", reject);
1386
+ req.setTimeout(options.timeout || 15e3, () => {
1387
+ req.destroy(new Error(`timeout of ${options.timeout || 15e3}ms exceeded`));
1388
+ });
1389
+ });
1390
+ }
1391
+ async ensureMetaObject() {
1392
+ const id = `${this.namespace}.meta`;
1393
+ try {
1394
+ const obj = await this.getObjectAsync(id);
1395
+ if (!obj) {
1396
+ await this.setObjectAsync(id, {
1397
+ type: "meta",
1398
+ common: {
1399
+ name: "JetFrame Files",
1400
+ type: "meta.user"
1401
+ },
1402
+ native: {}
1403
+ });
1404
+ this.log.info("Meta-Objekt f\xFCr Dateien erstellt");
1405
+ }
1406
+ } catch (e) {
1407
+ this.log.error(`Meta-Objekt Fehler: ${this.errorText(e)}`);
1408
+ }
1409
+ }
1410
+ logDebug(msg) {
1411
+ const config = (0, import_config.readConfig)(this);
1412
+ this.log.debug(`[JetFrame] ${msg}`);
1413
+ }
1414
+ logWarn(msg) {
1415
+ this.log.warn(`[JetFrame] \u26A0\uFE0F ${msg}`);
1416
+ }
1417
+ logError(msg) {
1418
+ this.log.error(`[JetFrame] \u274C ${msg}`);
1419
+ }
1420
+ clean(v) {
1421
+ return String(v || "").trim();
1422
+ }
1423
+ errorText(e) {
1424
+ if (!e) {
1425
+ return "unbekannter Fehler";
1426
+ }
1427
+ if (typeof e === "string") {
1428
+ return e;
1429
+ }
1430
+ if (e instanceof Error) {
1431
+ return e.message;
1432
+ }
1433
+ try {
1434
+ return JSON.stringify(e);
1435
+ } catch {
1436
+ return String(e);
1437
+ }
1438
+ }
1439
+ onUnload(callback) {
1440
+ try {
1441
+ this.clearTimer();
1442
+ this.clearStatisticsTimer();
1443
+ callback();
1444
+ } catch {
1445
+ callback();
1446
+ }
1447
+ }
1448
+ }
1449
+ if (require.main !== module) {
1450
+ module.exports = (options) => new Jetframe(options);
1451
+ } else {
1452
+ (() => new Jetframe())();
1453
+ }
1454
+ //# sourceMappingURL=main.js.map