[
    {
        "id": "7bdabe99d4cae0c7",
        "type": "tab",
        "label": "Flugzeug über Location",
        "disabled": false,
        "info": "## ✈️ Flugzeug über Location → AWTRIX\n\nZeigt Flugzeuge, die über deinem Zuhause fliegen, inkl. Route (Herkunft → Ziel) und Herkunftsland als kleine Flagge auf deinem AWTRIX-Gerät an.\n\n### Einrichtung\n\n1. Öffne den Node **⚙️ Konfiguration (hier anpassen!)** und trage ein:\n   - `homeLat` / `homeLon` – deine Koordinaten (z.B. von google maps)\n   - `radiusNm` – Suchradius in nautischen Meilen, je kleiner desto genauer \"über dem Haus\"\n   - `awtrixTopic` – MQTT-Topic deines AWTRIX-Geräts, z.B. `AWTRIX-xyz123/cmd/notify`\n   - `durationMs` – wie lange die Anzeige stehen bleibt (ms)\n   - `soundEnabled` – ob bei neuer Anzeige ein Ton kommt\n   - `soundRtttl` – der RTTTL-Klingelton-String\n   - `quietHoursStart` / `quietHoursEnd` – Nachtruhe (Stunde 0-23), in der kein Ton abgespielt wird\n   - `cooldownMinutes` – wie lange ein Flugzeug nach der ersten Anzeige gesperrt bleibt, damit es nicht doppelt gemeldet wird\n2. Öffne den MQTT-Broker Node am **\"An AWTRIX senden\"**-Node und trage die IP/Adresse deines Brokers ein.\n3. Deploy drücken – die Konfiguration wird direkt beim Start geladen.\n\n### Verwendete APIs\n- [adsb.lol](https://api.adsb.lol) – aktuelle Flugzeugpositionen (kostenlos, kein Key nötig)\n- [adsbdb.com](https://api.adsbdb.com) – Routeninformationen zum Callsign (kostenlos, kein Key nötig)\n\n### Doku zum AWTRIX-Gerät\nhttps://blueforcer.github.io/awtrix-ng/"
    },
    {
        "id": "cfg1n0dea1b2c3d4",
        "type": "inject",
        "z": "7bdabe99d4cae0c7",
        "name": "Beim Start konfigurieren",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": "0.1",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 160,
        "y": 40,
        "wires": [
            [
                "cfg2n0dea1b2c3d4"
            ]
        ]
    },
    {
        "id": "cfg2n0dea1b2c3d4",
        "type": "change",
        "z": "7bdabe99d4cae0c7",
        "name": "⚙️ Konfiguration (hier anpassen!)",
        "rules": [
            {
                "t": "set",
                "p": "homeLat",
                "pt": "flow",
                "to": "48.137",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "homeLon",
                "pt": "flow",
                "to": "11.575",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "radiusNm",
                "pt": "flow",
                "to": "5",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "awtrixTopic",
                "pt": "flow",
                "to": "AWTRIX-xyz123/cmd/notify",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "durationMs",
                "pt": "flow",
                "to": "50000",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "soundEnabled",
                "pt": "flow",
                "to": "true",
                "tot": "bool"
            },
            {
                "t": "set",
                "p": "soundRtttl",
                "pt": "flow",
                "to": "beep:d=4,o=6,b=200:16c",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "quietHoursStart",
                "pt": "flow",
                "to": "22",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "quietHoursEnd",
                "pt": "flow",
                "to": "9",
                "tot": "num"
            },
            {
                "t": "set",
                "p": "cooldownMinutes",
                "pt": "flow",
                "to": "10",
                "tot": "num"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "info": "### Erklärung der Einstellungen\n\n| Feld | Bedeutung |\n|---|---|\n| `homeLat` | Breitengrad deines Standorts (z.B. deines Hauses), als Dezimalzahl |\n| `homeLon` | Längengrad deines Standorts, als Dezimalzahl |\n| `radiusNm` | Suchradius um deinen Standort in nautischen Meilen. Kleinerer Wert = nur Flugzeuge direkt über dir, größerer Wert = auch Flugzeuge in der Umgebung |\n| `awtrixTopic` | Das MQTT-Topic deines AWTRIX-Geräts für Notifications, z.B. `AWTRIX-xyz123/cmd/notify`. Findest du in der AWTRIX-App/Weboberfläche unter den MQTT-Einstellungen |\n| `durationMs` | Wie lange die Meldung auf dem AWTRIX-Display angezeigt wird, in Millisekunden (50000 = 50 Sekunden) |\n| `soundEnabled` | `true`/`false` – ob bei einer neuen Flugzeug-Meldung ein Ton abgespielt werden soll |\n| `soundRtttl` | Der Klingelton als RTTTL-String, der abgespielt wird, wenn `soundEnabled` = true ist. Format siehe AWTRIX-Doku |\n| `quietHoursStart` | Stunde (0-23), ab der die Nachtruhe beginnt – ab dieser Uhrzeit wird kein Ton abgespielt, auch wenn `soundEnabled` = true ist |\n| `quietHoursEnd` | Stunde (0-23), ab der die Nachtruhe endet und wieder Ton abgespielt wird |\n| `cooldownMinutes` | Wie viele Minuten ein bereits gemeldetes Flugzeug gesperrt bleibt, bevor es erneut gemeldet werden kann (verhindert Doppel-Meldungen, wenn dasselbe Flugzeug mehrfach hintereinander erkannt wird) |\n\nDieser Node läuft einmalig beim Start (Deploy) und schreibt alle Werte in den Flow-Context. Nach einer Änderung hier muss neu deployed werden, damit die neuen Werte übernommen werden.",
        "x": 440,
        "y": 40,
        "wires": [
            []
        ]
    },
    {
        "id": "3faeb2e009e1adcd",
        "type": "inject",
        "z": "7bdabe99d4cae0c7",
        "name": "alle 20s",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "20",
        "crontab": "",
        "once": true,
        "onceDelay": "3",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 140,
        "y": 100,
        "wires": [
            [
                "df7dbb3217a0ec54"
            ]
        ]
    },
    {
        "id": "df7dbb3217a0ec54",
        "type": "function",
        "z": "7bdabe99d4cae0c7",
        "name": "Hausposition & Radius",
        "func": "// Werte kommen aus dem Node \"⚙️ Konfiguration\" (Flow-Context).\n// Falls dort nichts gesetzt ist, greifen diese Fallback-Werte.\nconst homeLat = flow.get('homeLat') ?? 48.137;\nconst homeLon = flow.get('homeLon') ?? 11.575;\nconst radiusNm = flow.get('radiusNm') ?? 5; // Umkreis - je kleiner, desto genauer \"über dem Haus\"\n\nmsg.url = `https://api.adsb.lol/v2/point/${homeLat}/${homeLon}/${radiusNm}`;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 380,
        "y": 100,
        "wires": [
            [
                "7feb4a24ac96d322"
            ]
        ]
    },
    {
        "id": "7feb4a24ac96d322",
        "type": "http request",
        "z": "7bdabe99d4cae0c7",
        "name": "ADS-B Abfrage",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 600,
        "y": 100,
        "wires": [
            [
                "28e579b74facb18d"
            ]
        ]
    },
    {
        "id": "28e579b74facb18d",
        "type": "function",
        "z": "7bdabe99d4cae0c7",
        "name": "Neue Flugzeuge filtern",
        "func": "const seen = flow.get('seenAircraft') || {};\nconst now = Date.now();\nconst cooldownMinutes = flow.get('cooldownMinutes') ?? 10;\nconst cooldownMs = cooldownMinutes * 60 * 1000; // Sperre pro Flugzeug\n\nfor (const hex in seen) {\n    if (now - seen[hex] > cooldownMs) delete seen[hex];\n}\n\nconst aircraft = (msg.payload && msg.payload.ac) || [];\nconst msgs = [];\n\nfor (const ac of aircraft) {\n    if (!ac.alt_baro || ac.alt_baro === \"ground\") continue; // nur Flugzeuge in der Luft\n    if (!ac.hex || seen[ac.hex]) continue;\n\n    const callsign = (ac.flight || \"\").trim();\n    if (!callsign) continue;\n\n    seen[ac.hex] = now;\n    msgs.push({ aircraftInfo: { callsign, altitude: ac.alt_baro, hex: ac.hex } });\n}\n\nflow.set('seenAircraft', seen);\nreturn [msgs.length ? msgs : null];",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 300,
        "y": 180,
        "wires": [
            [
                "39c51ab8b34591ba"
            ]
        ]
    },
    {
        "id": "39c51ab8b34591ba",
        "type": "function",
        "z": "7bdabe99d4cae0c7",
        "name": "Routen-Request vorbereiten",
        "func": "msg.url = `https://api.adsbdb.com/v0/callsign/${msg.aircraftInfo.callsign}`;\nreturn msg;",
        "outputs": 1,
        "x": 620,
        "y": 180,
        "wires": [
            [
                "bf460eb38650ad21"
            ]
        ]
    },
    {
        "id": "bf460eb38650ad21",
        "type": "http request",
        "z": "7bdabe99d4cae0c7",
        "name": "Routen-Abfrage (adsbdb)",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 870,
        "y": 180,
        "wires": [
            [
                "f0b0281172e34150"
            ]
        ]
    },
    {
        "id": "f0b0281172e34150",
        "type": "function",
        "z": "7bdabe99d4cae0c7",
        "name": "AWTRIX Payload bauen",
        "func": "// --- Flaggen als 8x8-Pixel-Icons (vereinfachte Näherung, keine Pixel-perfekten Flaggen) ---\nfunction stripesH(colors) {\n    const cmds = []; let y = 0;\n    colors.forEach((c, i) => {\n        const yEnd = Math.round(8 * (i + 1) / colors.length);\n        cmds.push([\"rectFill\", 0, y, 8, yEnd - y, c]);\n        y = yEnd;\n    });\n    return cmds;\n}\nfunction stripesV(colors) {\n    const cmds = []; let x = 0;\n    colors.forEach((c, i) => {\n        const xEnd = Math.round(8 * (i + 1) / colors.length);\n        cmds.push([\"rectFill\", x, 0, xEnd - x, 8, c]);\n        x = xEnd;\n    });\n    return cmds;\n}\nfunction nordicCross(bg, cross) {\n    return [[\"rectFill\", 0, 0, 8, 8, bg], [\"rectFill\", 0, 3, 8, 2, cross], [\"rectFill\", 2, 0, 2, 8, cross]];\n}\nfunction swissCross() {\n    return [[\"rectFill\", 0, 0, 8, 8, \"#FF0000\"], [\"rectFill\", 1, 3, 6, 2, \"#FFFFFF\"], [\"rectFill\", 3, 1, 2, 6, \"#FFFFFF\"]];\n}\nfunction turkeyFlag() {\n    return [[\"rectFill\", 0, 0, 8, 8, \"#E30A17\"], [\"circleFill\", 3, 4, 2, \"#FFFFFF\"], [\"circleFill\", 4, 4, 2, \"#E30A17\"], [\"pixel\", 6, 3, \"#FFFFFF\"]];\n}\nfunction ukFlag() {\n    return [[\"rectFill\", 0, 0, 8, 8, \"#00247D\"], [\"rectFill\", 0, 3, 8, 2, \"#FFFFFF\"], [\"rectFill\", 3, 0, 2, 8, \"#FFFFFF\"], [\"rectFill\", 0, 3, 8, 1, \"#CF142B\"], [\"rectFill\", 3, 0, 1, 8, \"#CF142B\"]];\n}\n\nconst FLAGS = {\n    DE: stripesH([\"#000000\", \"#DD0000\", \"#FFCC00\"]),\n    TR: turkeyFlag(),\n    ES: stripesH([\"#AA151B\", \"#F1BF00\", \"#AA151B\"]),\n    FR: stripesV([\"#0055A4\", \"#FFFFFF\", \"#EF4135\"]),\n    IT: stripesV([\"#009246\", \"#FFFFFF\", \"#CE2B37\"]),\n    GB: ukFlag(),\n    NL: stripesH([\"#AE1C28\", \"#FFFFFF\", \"#21468B\"]),\n    PL: stripesH([\"#FFFFFF\", \"#DC143C\"]),\n    AT: stripesH([\"#ED2939\", \"#FFFFFF\", \"#ED2939\"]),\n    CH: swissCross(),\n    GR: stripesH([\"#0D5EAF\", \"#FFFFFF\", \"#0D5EAF\", \"#FFFFFF\", \"#0D5EAF\"]),\n    PT: stripesV([\"#046A38\", \"#DA291C\"]),\n    BE: stripesV([\"#000000\", \"#FAE042\", \"#ED2939\"]),\n    SE: nordicCross(\"#006AA7\", \"#FECC00\"),\n    NO: nordicCross(\"#EF2B2D\", \"#FFFFFF\"),\n    DK: nordicCross(\"#C60C30\", \"#FFFFFF\"),\n    FI: nordicCross(\"#FFFFFF\", \"#003580\"),\n    IE: stripesV([\"#169B62\", \"#FFFFFF\", \"#FF883E\"]),\n    CZ: stripesH([\"#FFFFFF\", \"#D7141A\"]),\n    HR: stripesH([\"#FF0000\", \"#FFFFFF\", \"#171796\"]),\n    US: stripesH([\"#B22234\", \"#FFFFFF\", \"#3C3B6E\"]),\n    EG: stripesH([\"#CE1126\", \"#FFFFFF\", \"#000000\"]),\n    MA: [[\"rectFill\", 0, 0, 8, 8, \"#C1272D\"], [\"pixel\", 4, 4, \"#006233\"]]\n};\n\n// --- eigentliche Payload-Erstellung ---\nconst info = msg.aircraftInfo || {};\nconst callsign = info.callsign || \"?\";\nconst route = msg.payload && msg.payload.response && msg.payload.response.flightroute;\n\nif (!route) {\n    return null;\n}\n\nconst originCity = route.origin ? (route.origin.municipality || route.origin.iata_code) : \"?\";\nconst originIso = route.origin && route.origin.country_iso_name;\nconst destCity = route.destination ? (route.destination.municipality || route.destination.iata_code) : \"?\";\nconst destIso = route.destination && route.destination.country_iso_name;\n\nconst originLabel = originIso ? `${originCity} (${originIso})` : originCity;\nconst destLabel = destIso ? `${destCity} (${destIso})` : destCity;\n\nconst textFragments = [\n    { text: callsign, color: \"#FFCC00\" },\n    { text: \" | \", color: \"#666666\" },\n    { text: `${originLabel} -> ${destLabel}`, color: \"#FFFFFF\" }\n];\n\nlet draw = [], textOffsetX = 0;\nif (destIso && FLAGS[destIso]) {\n    draw = FLAGS[destIso];\n    textOffsetX = 9;\n}\n\n// Werte kommen aus dem Node \"⚙️ Konfiguration\" (Flow-Context), mit Fallbacks.\nconst awtrixTopic = flow.get('awtrixTopic') ?? \"AWTRIX-xyz123/cmd/notify\";\nconst durationMs = flow.get('durationMs') ?? 50000;\nconst soundEnabled = flow.get('soundEnabled') ?? true;\nconst soundRtttl = flow.get('soundRtttl') ?? \"beep:d=4,o=6,b=200:16c\";\nconst quietHoursStart = flow.get('quietHoursStart') ?? 22;\nconst quietHoursEnd = flow.get('quietHoursEnd') ?? 9;\n\n// Nachtruhe -> kein Piepton\nconst hour = new Date().getHours();\nconst isQuietHours = (hour >= quietHoursStart || hour < quietHoursEnd);\n\nmsg.topic = awtrixTopic;\nmsg.payload = {\n    text: textFragments,\n    draw: draw,\n    textOffsetX: textOffsetX,\n    durationMs: durationMs,\n    stack: true\n};\n\nif (soundEnabled && !isQuietHours) {\n    msg.payload.soundRtttl = soundRtttl;\n}\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1150,
        "y": 180,
        "wires": [
            [
                "1707fe3ccf05abf9"
            ]
        ]
    },
    {
        "id": "1707fe3ccf05abf9",
        "type": "mqtt out",
        "z": "7bdabe99d4cae0c7",
        "name": "An AWTRIX senden",
        "topic": "",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "mqtt_broker",
        "x": 1430,
        "y": 180,
        "wires": []
    },
    {
        "id": "mqtt_broker",
        "type": "mqtt-broker",
        "name": "",
        "broker": "",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]