NEWS
EPG-Daten via Script
-
Meinst du selected_channel ? Wenn ja, der bewirkt wenn dort zum beispiel die 33 eingetragen wird, dass du dann alle Sendungen von aktuell bis... von DMAX in der Liste bekommst.
Ro75.
@Ro75
noch ne Frage.
Wenn ich da jetzt einen Sender auswähle.
Wie bekomme ich den dann wieder weg. -
Wenn du meine Widget hast, dann nutze das Widget mit der Aufschrift "Was läuft gerade jetzt". Aber da musst du auch

den Datenpunkt entsprechend zuvor anpassen. Ansonsten trage in den Datenpunkt "broadcast_mode" all ein
Ro75.
@Ro75
Ja, das funktioniert ja.
Ich dachte nur, dass hier

der Wert dann auch gelöscht wird. -
Ich sage es mal so. Es zeigt alle Möglichkeiten. Das ist mit Sicherheit noch nicht der letzte Schrei. @sigi234 hat ja vielleicht auch dazu ein paar coole Ideen.
Ro75.
-
-
-
Verbesserungsvorschlag:
Im Script diese Zeilen (kommen 2 x vor):
const startTime = entry.start.split(" ")[1];
const stopTime = entry.stop.split(" ")[1];
ändern in:
const startTime = entry.start.split(" ")[1].substring(0,5);
const stopTime = entry.stop.split(" ")[1].substring(0,5);Dann wird Start und Stop ohne Sekunden angezeigt.
-
Hallo. Heute mal einen kleinen Einblick in mein neustes Projekt, mit etwas KI Unterstützung. Knapp 700 Zeilen Code liefern das hier:


Ca. 170 Sender. Favoritensteuerung, Senderreihenfolge, automatische Aktualisierung.

Oder alle anstehenden Sendungen zu einem Sender.EPG-TV für ioBroker
Mit diesem Script lassen sich aktuelle TV-Programmdaten (EPG) von über 160 deutschen TV-Sendern direkt in ioBroker integrieren. Die Daten werden automatisch aus einer XMLTV-Quelle geladen und als Datenpunkte zur Verfügung gestellt.
Neben den eigentlichen Programminformationen bietet das Script eine komfortable Senderverwaltung mit Favoritenfunktion, individueller Sortierung sowie umfangreichen Filtermöglichkeiten. Dadurch eignet es sich hervorragend für Visualisierungen in Jarvis, VIS oder anderen Dashboards.
Funktionen
- Über 160 deutsche TV-Sender
- Free-TV und Pay-TV
- Automatischer Import der EPG-Daten
- Aktuelle und kommende Sendungen
- Senderlogos
- Favoritenverwaltung
- Individuelle Senderreihenfolge
- Filter nach Senderart und Kategorie
- Automatische tägliche Aktualisierung
Voraussetzungen
Für das Script werden folgende zusätzliche NPM-Module benötigt:
Modul Verwendung xml2jsEinlesen und Verarbeiten der XMLTV-Daten user-agentsErzeugung eines Browser-User-Agents für den Abruf der EPG-Daten Die Module können im JavaScript-Adapter unter „Zusätzliche NPM-Module“ eingetragen werden. Anschließend den JavaScript-Adapter neu starten.
Visualisierung
Die Ausgabe erfolgt hauptsächlich über den Datenpunkt
broadcast_list.Für die Darstellung verwende ich das JSON-Widget aus VIS Material Design ab Version 0.5.94. Die Programmdaten werden bereits als fertiges JSON bereitgestellt und können direkt im Widget verarbeitet werden.
Wichtige Hinweise
Beim ersten Start werden automatisch alle verfügbaren Sender aus der XMLTV-Quelle eingelesen und die benötigten Datenpunkte angelegt.
Folgende Datenpunkte werden vollständig vom Script verwaltet und sollten nicht manuell geändert werden:
sender_jsonactive_sender_listfavorite_sender_listbroadcast_list
Die Senderbearbeitung erfolgt ausschließlich über die dafür vorgesehenen Datenpunkte im Bereich
edit.*.Wichtige Datenpunkte
Datenpunkt Bedeutung Verwendung sender_jsonInterne Senderliste Wird vom Script zur Senderverwaltung verwendet active_sender_listAlle aktiven Sender Für Dropdowns und Senderauswahl favorite_sender_listAlle Favoritensender Für Favoritenlisten und Senderauswahl broadcast_listProgrammausgabe Hauptdatenpunkt für Tabellen, Listen und TV-Guides selected_channelAusgewählter Sender Zeigt aktuelle und kommende Sendungen eines Senders refreshManueller Reload Lädt Sender- und EPG-Daten neu filter_*Filterfunktionen Filterung nach Favoriten, Free-/Pay-TV und Kategorien broadcast_modeAnzeigemodus all= alle Sender,selected= ausgewählter SendercountAnzahl gefundener Sender Kontrollwert nach dem Einlesen max_order_numberHöchste vergebene Sendernummer Hilfswert für die Senderreihenfolge Verfügbare Filter
- Favoriten
- Free-TV
- Pay-TV
- Film
- Serie
- Doku
- Kinder
- Musik
- Shopping
- Sport
- Nachrichten
- Vollprogramm
Automatische Aktualisierung
- Täglicher EPG-Import um 03:00 Uhr
- Manueller Reload über
refresh - Laufende Aktualisierung der Programmliste
Verwendung
Alle aktuellen Sendungen anzeigen
broadcast_mode = allZeigt die aktuell laufenden Sendungen aller aktiven Sender in
broadcast_list.Einzelnen Sender anzeigen
selected_channelauf die gewünschte Sender-ID setzen.Anschließend enthält
broadcast_listdie aktuelle sowie alle kommenden Sendungen dieses Senders.Favoriten verwenden
Sender können als Favoriten markiert und über
favorite_sender_listoder den Filterfilter_favoriteverwendet werden.Sender filtern
Die Ausgabe kann über die verschiedenen
filter_*Datenpunkte nach Senderart oder Kategorie eingeschränkt werden.Hinweis
Dieses Script wurde mit Unterstützung von KI entwickelt und anschließend in ioBroker getestet und angepasst. Trotz umfangreicher Tests können Fehler oder Verbesserungspotenziale nicht ausgeschlossen werden. Rückmeldungen, Fehlerberichte und Verbesserungsvorschläge sind daher jederzeit willkommen.Hier das Script:
// === EPG - TV-Daten === //Erst-Version 1.0.0 - 22.06.2026 //Version 1.0.1 - 24.06.2026 //Ersteller Ro75. //Voraussetzungen (Version 1.0.1 getestet mit) //NodeJS: 22.22.x //Javascript-Adapter: 9.0.18 //Admin-Adapter: 7.8.23 //js-controler: 7.2.2 //********** START KONFIGURATION ********** const root = "0_userdata.0.epg."; const data = root + "data."; const sender = root + "sender_json"; //********** AB HIER NICHTS MEHR ÄNDERN ********** const xml2js = require("xml2js"); const UserAgent = require("user-agents"); let select_channel_mode = 0; let loading = false; const CONFIG = { "1": { url_id: "1-2-3tv.de", art: "Free", typ: "Shopping", name: "1-2-3.tv" }, "2": { url_id: "13THSTREET.de", art: "Pay", typ: "Film", name: "13th Street" }, "3": { url_id: "3sat.de", art: "Free", typ: "Vollprogramm", name: "3sat" }, "4": { url_id: "atv.de", art: "Free", typ: "Vollprogramm", name: "ATV" }, "5": { url_id: "AnimalPlanet.de", art: "Pay", typ: "Doku", name: "Animal Planet" }, "6": { url_id: "ANIXESerie.de", art: "Free", typ: "Serie", name: "ANIXE Serie" }, "7": { url_id: "ANIXE+.de", art: "Free", typ: "Vollprogramm", name: "ANIXE+" }, "8": { url_id: "ARD-alpha.de", art: "Free", typ: "Doku", name: "ARD alpha" }, "9": { url_id: "ARTE.de", art: "Free", typ: "Doku", name: "ARTE" }, "10": { url_id: "automotorundsport.de", art: "Pay", typ: "Doku", name: "auto motor und sport" }, "11": { url_id: "AXNBlack.de", art: "Pay", typ: "Film", name: "AXN Black" }, "12": { url_id: "AXNWhite.de", art: "Pay", typ: "Serie", name: "AXN White" }, "13": { url_id: "Beate-UhseTV.de", art: "Pay", typ: "Film", name: "Beate-Uhse.TV" }, "14": { url_id: "BR.de", art: "Free", typ: "Vollprogramm", name: "BR Fernsehen" }, "15": { url_id: "CartoonNetwork.de", art: "Pay", typ: "Kinder", name: "Cartoon Network" }, "16": { url_id: "Cartoonito.de", art: "Pay", typ: "Kinder", name: "Cartoonito" }, "17": { url_id: "ComedyCentral.de", art: "Free", typ: "Serie", name: "Comedy Central" }, "18": { url_id: "Crime+Investigation.de", art: "Pay", typ: "Doku", name: "Crime + Investigation" }, "19": { url_id: "CuriosityChannel.de", art: "Pay", typ: "Doku", name: "Curiosity Channel" }, "20": { url_id: "DasErste.de", art: "Free", typ: "Vollprogramm", name: "Das Erste" }, "21": { url_id: "DAZN.de", art: "Pay", typ: "Sport", name: "DAZN" }, "22": { url_id: "DAZN1.de", art: "Pay", typ: "Sport", name: "DAZN 1" }, "23": { url_id: "DAZN2.de", art: "Pay", typ: "Sport", name: "DAZN 2" }, "24": { url_id: "DeluxeDanceByKontor.de", art: "Free", typ: "Musik", name: "DELUXE DANCE by Kontor" }, "25": { url_id: "DeluxeFlashback.de", art: "Free", typ: "Musik", name: "DELUXE FLASHBACK" }, "26": { url_id: "DeluxeLounge.de", art: "Free", typ: "Musik", name: "DELUXE LOUNGE" }, "27": { url_id: "DeluxeMusic.de", art: "Free", typ: "Musik", name: "DELUXE MUSIC" }, "28": { url_id: "DeluxeRap.de", art: "Free", typ: "Musik", name: "DELUXE RAP" }, "29": { url_id: "DeluxeRock.de", art: "Free", typ: "Musik", name: "DELUXE ROCK" }, "30": { url_id: "DeutschesMusikFernsehen.de",art: "Free", typ: "Musik", name: "Deutsches Musik Fernsehen" }, "31": { url_id: "Discovery.de", art: "Pay", typ: "Doku", name: "Discovery Channel" }, "32": { url_id: "DisneyChannel.de", art: "Free", typ: "Kinder", name: "Disney Channel" }, "33": { url_id: "DMAX.de", art: "Free", typ: "Doku", name: "DMAX" }, "34": { url_id: "dokusat.de", art: "Free", typ: "Doku", name: "dokuSAT" }, "35": { url_id: "EDGESport.de", art: "Pay", typ: "Sport", name: "EDGE Sport" }, "36": { url_id: "emsTV.de", art: "Free", typ: "Musik", name: "ems TV" }, "37": { url_id: "eSPORTS1.de", art: "Pay", typ: "Sport", name: "eSPORTS1" }, "38": { url_id: "Eurosport1.de", art: "Free", typ: "Sport", name: "Eurosport 1" }, "39": { url_id: "Eurosport2.de", art: "Pay", typ: "Sport", name: "Eurosport 2" }, "40": { url_id: "Heimatkanal.de", art: "Pay", typ: "Film", name: "Heimatkanal" }, "41": { url_id: "HGTV.de", art: "Free", typ: "Doku", name: "HGTV" }, "42": { url_id: "HISTORY.de", art: "Pay", typ: "Doku", name: "HISTORY" }, "43": { url_id: "hr.de", art: "Free", typ: "Vollprogramm", name: "hr Fernsehen" }, "44": { url_id: "HSE.de", art: "Free", typ: "Shopping", name: "HSE" }, "45": { url_id: "HSE24.de", art: "Free", typ: "Shopping", name: "HSE24" }, "46": { url_id: "Jukebox.de", art: "Free", typ: "Musik", name: "Jukebox" }, "47": { url_id: "JustCooking.de", art: "Free", typ: "Doku", name: "Just Cooking" }, "48": { url_id: "KabelEins.de", art: "Free", typ: "Vollprogramm", name: "kabel eins" }, "49": { url_id: "KabelEinsCLASSICS.de", art: "Pay", typ: "Film", name: "kabel eins CLASSICS" }, "50": { url_id: "KabelEinsDoku.de", art: "Free", typ: "Doku", name: "kabel eins Doku" }, "51": { url_id: "KiKA.de", art: "Free", typ: "Kinder", name: "KiKA" }, "52": { url_id: "KinoweltTV.de", art: "Pay", typ: "Film", name: "Kinowelt TV" }, "53": { url_id: "MDRSachsen.de", art: "Free", typ: "Vollprogramm", name: "MDR Sachsen" }, "54": { url_id: "MTV.de", art: "Pay", typ: "Musik", name: "MTV" }, "55": { url_id: "MTV00s.de", art: "Pay", typ: "Musik", name: "MTV 00s" }, "56": { url_id: "MTV80s.de", art: "Pay", typ: "Musik", name: "MTV 80s" }, "57": { url_id: "MTV90s.de", art: "Pay", typ: "Musik", name: "MTV 90s" }, "58": { url_id: "MTVHits.de", art: "Pay", typ: "Musik", name: "MTV Hits" }, "59": { url_id: "MTVLive.de", art: "Pay", typ: "Musik", name: "MTV Live" }, "60": { url_id: "n-tv.de", art: "Free", typ: "Doku", name: "n-tv" }, "61": { url_id: "N24Doku.de", art: "Free", typ: "Doku", name: "N24 Doku" }, "62": { url_id: "NationalGeographic.de", art: "Pay", typ: "Doku", name: "National Geographic" }, "63": { url_id: "NationalGeographicWILD.de", art: "Pay", typ: "Doku", name: "Nat Geo Wild" }, "64": { url_id: "NDR.de", art: "Free", typ: "Vollprogramm", name: "NDR Fernsehen" }, "65": { url_id: "Nick.de", art: "Free", typ: "Kinder", name: "Nick" }, "66": { url_id: "NickComedyCentral+1.de", art: "Free", typ: "Kinder", name: "Nick / CC +1" }, "67": { url_id: "NickJr.de", art: "Pay", typ: "Kinder", name: "Nick Jr." }, "68": { url_id: "Nicktoons.de", art: "Pay", typ: "Kinder", name: "Nicktoons" }, "69": { url_id: "NITRO.de", art: "Free", typ: "Vollprogramm", name: "NITRO" }, "70": { url_id: "ONE.de", art: "Free", typ: "Vollprogramm", name: "ONE" }, "71": { url_id: "phoenix.de", art: "Free", typ: "Doku", name: "phoenix" }, "72": { url_id: "ProSieben.de", art: "Free", typ: "Vollprogramm", name: "ProSieben" }, "73": { url_id: "ProSiebenFUN.de", art: "Pay", typ: "Serie", name: "ProSieben FUN" }, "74": { url_id: "ProSiebenMAXX.de", art: "Free", typ: "Vollprogramm", name: "ProSieben MAXX" }, "75": { url_id: "QVC.de", art: "Free", typ: "Shopping", name: "QVC" }, "76": { url_id: "rbbBrandenburg.de", art: "Free", typ: "Vollprogramm", name: "rbb Brandenburg" }, "77": { url_id: "RiC.de", art: "Free", typ: "Kinder", name: "RiC" }, "78": { url_id: "RomanceTV.de", art: "Pay", typ: "Film", name: "Romance TV" }, "79": { url_id: "RTL.de", art: "Free", typ: "Vollprogramm", name: "RTL" }, "80": { url_id: "RTLCrime.de", art: "Pay", typ: "Serie", name: "RTL Crime" }, "81": { url_id: "RTLLiving.de", art: "Pay", typ: "Doku", name: "RTL Living" }, "82": { url_id: "RTLPassion.de", art: "Pay", typ: "Serie", name: "RTL Passion" }, "83": { url_id: "RTLplus.de", art: "Free", typ: "Serie", name: "RTLplus" }, "84": { url_id: "RTLup.de", art: "Free", typ: "Vollprogramm", name: "RTLup" }, "85": { url_id: "RTLZWEI.de", art: "Free", typ: "Vollprogramm", name: "RTLZWEI" }, "86": { url_id: "SAT1.de", art: "Free", typ: "Vollprogramm", name: "SAT.1" }, "87": { url_id: "SAT1emotions.de", art: "Pay", typ: "Serie", name: "SAT.1 emotions" }, "88": { url_id: "SAT1GOLD.de", art: "Free", typ: "Vollprogramm", name: "SAT.1 GOLD" }, "89": { url_id: "SchlagerDeluxe.de", art: "Free", typ: "Musik", name: "Schlager Deluxe" }, "90": { url_id: "SchlagerparadiesTV.de", art: "Free", typ: "Musik", name: "Schlagerparadies TV" }, "91": { url_id: "sixx.de", art: "Free", typ: "Vollprogramm", name: "sixx" }, "92": { url_id: "SkyCinemaAction.de", art: "Pay", typ: "Film", name: "Sky Cinema Action" }, "93": { url_id: "SkyCinemaBlockbuster.de", art: "Pay", typ: "Film", name: "Sky Cinema Blockbuster" }, "94": { url_id: "SkyCinemaClassics.de", art: "Pay", typ: "Film", name: "Sky Cinema Classics" }, "96": { url_id: "SkyCinemaPremiere.de", art: "Pay", typ: "Film", name: "Sky Cinema Premiere" }, "97": { url_id: "SkyCrime.de", art: "Pay", typ: "Serie", name: "Sky Crime" }, "98": { url_id: "SkyDocumentaries.de", art: "Pay", typ: "Doku", name: "Sky Documentaries" }, "99": { url_id: "SkyNature.de", art: "Pay", typ: "Doku", name: "Sky Nature" }, "100": { url_id: "SkyOne.de", art: "Pay", typ: "Vollprogramm", name: "Sky One" }, "101": { url_id: "SkySciFi.de", art: "Pay", typ: "Film", name: "Sky Sci-Fi" }, "102": { url_id: "SkyShowcase.de", art: "Pay", typ: "Vollprogramm", name: "Sky Showcase" }, "103": { url_id: "SkySport1.de", art: "Pay", typ: "Sport", name: "Sky Sport 1" }, "104": { url_id: "SkySport10.de", art: "Pay", typ: "Sport", name: "Sky Sport 10" }, "105": { url_id: "SkySport2.de", art: "Pay", typ: "Sport", name: "Sky Sport 2" }, "106": { url_id: "SkySport3.de", art: "Pay", typ: "Sport", name: "Sky Sport 3" }, "107": { url_id: "SkySport4.de", art: "Pay", typ: "Sport", name: "Sky Sport 4" }, "108": { url_id: "SkySport5.de", art: "Pay", typ: "Sport", name: "Sky Sport 5" }, "109": { url_id: "SkySport6.de", art: "Pay", typ: "Sport", name: "Sky Sport 6" }, "110": { url_id: "SkySport7.de", art: "Pay", typ: "Sport", name: "Sky Sport 7" }, "111": { url_id: "SkySport8.de", art: "Pay", typ: "Sport", name: "Sky Sport 8" }, "112": { url_id: "SkySport9.de", art: "Pay", typ: "Sport", name: "Sky Sport 9" }, "113": { url_id: "SkySportAustria1.de", art: "Pay", typ: "Sport", name: "Sky Sport Austria 1" }, "114": { url_id: "SkySportBundesliga.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga" }, "115": { url_id: "SkySportBundesliga1.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 1" }, "116": { url_id: "SkySportBundesliga10.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 10" }, "117": { url_id: "SkySportBundesliga2.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 2" }, "118": { url_id: "SkySportBundesliga3.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 3" }, "119": { url_id: "SkySportBundesliga4.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 4" }, "120": { url_id: "SkySportBundesliga5.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 5" }, "121": { url_id: "SkySportBundesliga6.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 6" }, "122": { url_id: "SkySportBundesliga7.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 7" }, "123": { url_id: "SkySportBundesliga8.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 8" }, "124": { url_id: "SkySportBundesliga9.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 9" }, "125": { url_id: "SkySportBundesligaUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga UHD" }, "126": { url_id: "SkySportF1.de", art: "Pay", typ: "Sport", name: "Sky Sport F1" }, "127": { url_id: "SkySportGolf.de", art: "Pay", typ: "Sport", name: "Sky Sport Golf" }, "128": { url_id: "SkySportKompakt1.de", art: "Pay", typ: "Sport", name: "Sky Sport Kompakt 1" }, "129": { url_id: "SkySportMix.de", art: "Pay", typ: "Sport", name: "Sky Sport Mix" }, "130": { url_id: "SkySportNews.de", art: "Pay", typ: "Sport", name: "Sky Sport News" }, "131": { url_id: "SkySportPremierLeague.de", art: "Pay", typ: "Sport", name: "Sky Sport Premier League" }, "132": { url_id: "SkySportTennis.de", art: "Pay", typ: "Sport", name: "Sky Sport Tennis" }, "133": { url_id: "SkySportTopEvent.de", art: "Pay", typ: "Sport", name: "Sky Sport Top Event" }, "134": { url_id: "SkySportUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport UHD" }, "135": { url_id: "SkyAtlantic.de", art: "Pay", typ: "Serie", name: "Sky Atlantic" }, "136": { url_id: "SkyKrimi.de", art: "Pay", typ: "Serie", name: "Sky Krimi" }, "137": { url_id: "sonnenklarTV.de", art: "Free", typ: "Shopping", name: "sonnenklar.TV" }, "138": { url_id: "SpiegelGeschichte.de", art: "Pay", typ: "Doku", name: "Spiegel Geschichte" }, "139": { url_id: "SPORT1.de", art: "Free", typ: "Sport", name: "SPORT1" }, "140": { url_id: "SPORT1+.de", art: "Pay", typ: "Sport", name: "SPORT1+" }, "141": { url_id: "SRFernsehen.de", art: "Free", typ: "Vollprogramm", name: "SR Fernsehen" }, "142": { url_id: "SuperRTL.de", art: "Free", typ: "Kinder", name: "SUPER RTL" }, "143": { url_id: "SWRBW.de", art: "Free", typ: "Vollprogramm", name: "SWR BW" }, "144": { url_id: "tagesschau24.de", art: "Free", typ: "Doku", name: "tagesschau24" }, "145": { url_id: "Tele5.de", art: "Free", typ: "Film", name: "Tele 5" }, "146": { url_id: "TLC.de", art: "Free", typ: "Doku", name: "TLC" }, "147": { url_id: "TOGGOplus.de", art: "Free", typ: "Kinder", name: "TOGGO plus" }, "148": { url_id: "UniversalTV.de", art: "Pay", typ: "Serie", name: "Universal TV" }, "149": { url_id: "VolksmusikTV.de", art: "Free", typ: "Musik", name: "Volksmusik TV" }, "150": { url_id: "VOX.de", art: "Free", typ: "Vollprogramm", name: "VOX" }, "151": { url_id: "VOXup.de", art: "Free", typ: "Vollprogramm", name: "VOXup" }, "152": { url_id: "WaidwerkTV.de", art: "Pay", typ: "Doku", name: "Waidwerk TV" }, "153": { url_id: "WarnerTVComedy.de", art: "Pay", typ: "Serie", name: "Warner TV Comedy" }, "154": { url_id: "WarnerTVFilm.de", art: "Pay", typ: "Film", name: "Warner TV Film" }, "155": { url_id: "WarnerTVSerie.de", art: "Pay", typ: "Serie", name: "Warner TV Serie" }, "156": { url_id: "WDR.de", art: "Free", typ: "Vollprogramm", name: "WDR Fernsehen" }, "157": { url_id: "WELT.de", art: "Free", typ: "Doku", name: "WELT" }, "158": { url_id: "WeltderWunder.de", art: "Free", typ: "Doku", name: "Welt der Wunder" }, "159": { url_id: "ZDF.de", art: "Free", typ: "Vollprogramm", name: "ZDF" }, "160": { url_id: "zdf_neo.de", art: "Free", typ: "Vollprogramm", name: "ZDFneo" }, "161": { url_id: "ZDFinfo.de", art: "Free", typ: "Doku", name: "ZDFinfo" } }; async function smartCreateState(id, value, options = {}) { if (existsState(id)) return; await createState(id, value, options); } function sleepMs(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const MAP_URL_TO_ID = {}; for (const key in CONFIG) MAP_URL_TO_ID[CONFIG[key].url_id] = Number(key); function parseXmltvTime(t) { const [stamp, offset] = t.split(" "); const d = new Date(`${stamp.slice(0,4)}-${stamp.slice(4,6)}-${stamp.slice(6,8)}T${stamp.slice(8,10)}:${stamp.slice(10,12)}:${stamp.slice(12,14)}${offset}`); return `${String(d.getDate()).padStart(2,"0")}.${String(d.getMonth()+1).padStart(2,"0")}.${d.getFullYear()} ` + `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}`; } function parseGermanDateTime(str) { const [datePart, timePart] = str.split(" "); const [day, month, year] = datePart.split(".").map(Number); const [hour, minute, second] = timePart.split(":").map(Number); return new Date(year, month - 1, day, hour, minute, second).getTime(); } function setStateIfChanged(id, value, STOPPED = false) { if (STOPPED) return; const state = getState(id); if (!state) return; const old = state.val; const isObject = v => v !== null && typeof v === "object"; if (isObject(value)) { const newStr = JSON.stringify(value); let oldStr = null; if (typeof old === "string") oldStr = old; else if (isObject(old)) oldStr = JSON.stringify(old); if (newStr === oldStr) return; return setState(id, newStr, true); } if (old === value) return; setState(id, value, true); } async function main() { loading = true; await smartCreateState(sender, "[]", {name: "Senderliste", type: "string", read: true, write: false}); await smartCreateState(root + "count", 0, {name: "Anzahl", type: "number", read: true, write: false}); await smartCreateState(root + "refresh", false, {name: "Aktualisieren", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_favorite", false, {name: "Filter nur Favoriten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_free", false, {name: "Filter nur Free-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_pay", false, {name: "Filter nur Pay-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_film", false, {name: "Filter Typ Film", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_serie", false, {name: "Filter Typ Serie", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_doku", false, {name: "Filter Typ Doku", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_kinder", false, {name: "Filter Typ Kinder", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_musik", false, {name: "Filter Typ Musik", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_shopping", false, {name: "Filter Typ Shopping", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_sport", false, {name: "Filter Typ Sport", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_nachrichten", false, {name: "Filter Typ Nachrichten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_vollprogramm", false, {name: "Filter Typ Vollprogramm", type: "boolean", read: true, write: true}); await smartCreateState(root + "broadcast_mode", "all", {name: "Broadcast Modus", type: "string", read: true, write: true}); await smartCreateState(root + "broadcast_list", "[]", {name: "aktuelle Sendungen", type: "string", read: true, write: false}); await smartCreateState(root + "selected_channel", 0, {name: "Ausgewählter Sender", type: "number", read: true, write: true}); await smartCreateState(root + "selected_channel_favorite", 0, {name: "Ausgewählter Favorit-Sender", type: "number", read: true, write: true}); await smartCreateState(root + "active_sender_list", "[]", {name: "alle aktiven Sender mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "favorite_sender_list", "[]", {name: "alle Favoriten mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "max_order_number", 0, {name: "Höchste vergebene Sendernummer", type: "number", read: true, write: false}); await smartCreateState(root + "sender_edit_list", "[]", {name: "Editierungsliste", type: "string", read: true, write: true}); await smartCreateState(root + "edit.active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.icon_url", "", { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(root + "edit.art", "", { name: "Art", type: "string", read: true, write: false }); await smartCreateState(root + "edit.typ", "", { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(root + "edit.name", "", { name: "Name", type: "string", read: true, write: true }); await smartCreateState(root + "edit.order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); await smartCreateState(root + "edit.save", false, { name: "Speichern", type: "boolean", read: true, write: true }); await sleepMs(200); await loadChannelsAndProgrammes(); await sleepMs(500); await createSenderList("active"); await createSenderList("favorite"); await sleepMs(200); await initSenderEditList(); await sleepMs(2000); await updateAllActiveBroadcasts(); registerTriggers(); loading = false; } main(); function senderPassesFilters(internal_id) { const base = data + internal_id; const active = getState(base + ".active")?.val; if (!active) return false; const favoriteOnly = getState(root + "filter_favorite")?.val === true; if (favoriteOnly) { const fav = getState(base + ".favorite")?.val; if (!fav) return false; } const allowedArt = []; if (getState(root + "filter_free")?.val === true) allowedArt.push("Free"); if (getState(root + "filter_pay")?.val === true) allowedArt.push("Pay"); const art = getState(base + ".art")?.val; if (allowedArt.length > 0 && !allowedArt.includes(art)) return false; const senderTyp = getState(base + ".typ")?.val; const allowedTypes = []; if (getState(root + "filter_typ_film")?.val === true) allowedTypes.push("Film"); if (getState(root + "filter_typ_serie")?.val === true) allowedTypes.push("Serie"); if (getState(root + "filter_typ_doku")?.val === true) allowedTypes.push("Doku"); if (getState(root + "filter_typ_kinder")?.val === true) allowedTypes.push("Kinder"); if (getState(root + "filter_typ_musik")?.val === true) allowedTypes.push("Musik"); if (getState(root + "filter_typ_shopping")?.val === true) allowedTypes.push("Shopping"); if (getState(root + "filter_typ_sport")?.val === true) allowedTypes.push("Sport"); if (getState(root + "filter_typ_nachrichten")?.val === true) allowedTypes.push("Nachrichten"); if (getState(root + "filter_typ_vollprogramm")?.val === true) allowedTypes.push("Vollprogramm"); if (allowedTypes.length > 0 && !allowedTypes.includes(senderTyp)) return false; return true; } async function updateMaxOrderNumber() { const senderList = JSON.parse(getState(sender).val || "[]"); let max = 0; for (const s of senderList) { const order = getState(data + s.internal_id + ".order")?.val || 0; if (order > max) max = order; } setStateIfChanged(root + "max_order_number", max); return max; } async function loadSenderIntoEdit(id) { const base = data + id; setStateIfChanged(root + "edit.active", getState(base + ".active")?.val || false); setStateIfChanged(root + "edit.favorite", getState(base + ".favorite")?.val || false); setStateIfChanged(root + "edit.icon_url", getState(base + ".icon_url")?.val || ""); setStateIfChanged(root + "edit.art", getState(base + ".art")?.val || ""); setStateIfChanged(root + "edit.typ", getState(base + ".typ")?.val || ""); setStateIfChanged(root + "edit.name", getState(base + ".name")?.val || ""); setStateIfChanged(root + "edit.order", getState(base + ".order")?.val || 0); } async function saveEditToSender(id) { const base = data + id; setStateIfChanged(base + ".active", getState(root + "edit.active")?.val); setStateIfChanged(base + ".favorite", getState(root + "edit.favorite")?.val); setStateIfChanged(base + ".order", getState(root + "edit.order")?.val); } async function createEditSenderList() { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; const name = getState(base + ".name")?.val || ""; const order = Number(getState(base + ".order")?.val || 0); const favorite = getState(base + ".favorite")?.val === true; const active = getState(base + ".active")?.val === true; result.push({ text: name, value: s.internal_id, order: order, favorite: favorite, active: active }); } result.sort((a, b) => { const oa = a.order; const ob = b.order; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.text.localeCompare(b.text); }); return result; } async function initSenderEditList() { const list = await createEditSenderList(); setStateIfChanged(root + "sender_edit_list", JSON.stringify(list)); } async function loadChannelsAndProgrammes() { try { const ua = new UserAgent().toString(); const response = await httpGetAsync("https://iptv-epg.org/files/epg-de.xml", { timeout: 20000, headers: { "User-Agent": ua } }); if (!response || response.statusCode !== 200) return; const xml = response.data; const result = await xml2js.parseStringPromise(xml); const channels = result.tv.channel .filter(ch => MAP_URL_TO_ID[ch.$.id]) .map(ch => ({ id: ch.$.id, name: Array.isArray(ch["display-name"]) ? (typeof ch["display-name"][0] === "string" ? ch["display-name"][0] : ch["display-name"][0]._ || "") : "", internal_id: MAP_URL_TO_ID[ch.$.id], icon_url: ch.icon?.[0]?.$.src || "" })) .sort((a, b) => a.internal_id - b.internal_id); setStateIfChanged(root + "count", channels.length); setStateIfChanged(sender, JSON.stringify(channels.map(c => ({id: c.id, name: CONFIG[c.internal_id].name, internal_id: c.internal_id})))); const programmes = result.tv.programme .filter(p => MAP_URL_TO_ID[p.$.channel]) .map(p => ({ internal_id: MAP_URL_TO_ID[p.$.channel], start: parseXmltvTime(p.$.start), stop: parseXmltvTime(p.$.stop), title: Array.isArray(p.title) ? (p.title[0]._ || p.title[0]) : "", desc: Array.isArray(p.desc) ? (p.desc[0]._ || p.desc[0]) : "", date: Array.isArray(p.date) ? p.date[0] : "", episode: Array.isArray(p["episode-num"]) ? p["episode-num"][0] : "", categories: Array.isArray(p.category) ? p.category.map(c => (typeof c === "string" ? c : c._ || "")) : [], icon: p.icon?.[0]?.$.src || "" })); const grouped = {}; for (const p of programmes) { if (!grouped[p.internal_id]) grouped[p.internal_id] = []; grouped[p.internal_id].push(p); } for (const ch of channels) { const list = grouped[ch.internal_id] || []; const base = data + ch.internal_id; const now = Date.now(); const filtered = list.filter(p => { const stopTs = new Date( p.stop.slice(6,10) + "-" + p.stop.slice(3,5) + "-" + p.stop.slice(0,2) + "T" + p.stop.slice(11,19) ).getTime(); return stopTs >= now; }); // internal_id aus JSON entfernen const cleaned = filtered.map(p => { const { internal_id, ...rest } = p; return rest; }); await smartCreateState(base + ".broadcast", "[]", { name: "Sendungen", type: "string", read: true, write: false }); await smartCreateState(base + ".active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(base + ".favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(base + ".icon_url", ch.icon_url, { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(base + ".art", CONFIG[ch.internal_id].art, { name: "Art", type: "string", read: true, write: false }); await smartCreateState(base + ".typ", CONFIG[ch.internal_id].typ, { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(base + ".name", CONFIG[ch.internal_id].name, { name: "Name", type: "string", read: true, write: true }); await smartCreateState(base + ".order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); setStateIfChanged(base + ".broadcast", JSON.stringify(cleaned)); } } catch (err) { log(err, "error"); } } async function createSenderList(mode) { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; if (mode === "favorite") { const favorite = getState(base + ".favorite")?.val; if (!favorite) continue; } const senderName = getState(base + ".name")?.val || ""; result.push({text: senderName, value: s.internal_id }); } if (mode === "favorite") { result.sort((a, b) => { const orderA = getState(data + a.value + ".order")?.val || 0; const orderB = getState(data + b.value + ".order")?.val || 0; if (orderA > 0 && orderB === 0) return -1; if (orderA === 0 && orderB > 0) return 1; if (orderA > 0 && orderB > 0) return orderA - orderB; return a.text.localeCompare(b.text); }); } else { result.sort((a, b) => a.text.localeCompare(b.text)); } if (mode === "active") { setStateIfChanged(root + "active_sender_list", JSON.stringify(result)); } else if (mode === "favorite") { setStateIfChanged(root + "favorite_sender_list", JSON.stringify(result)); } return result; } async function updateAllActiveBroadcasts() { const result = []; const now = Date.now(); const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { if (!senderPassesFilters(s.internal_id)) continue; const base = data + s.internal_id; const order = getState(base + ".order")?.val || 0; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für Sender " + s.internal_id + ": " + e, "warn"); continue; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const start_ts = parseGermanDateTime(entry.start); const stop_ts = parseGermanDateTime(entry.stop); if (now >= start_ts && now < stop_ts) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon, order: order }); break; } } } result.sort((a, b) => { const oa = a.order || 0; const ob = b.order || 0; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.sender_name.localeCompare(b.sender_name); }); setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } async function updateSelectedChannelBroadcast() { const result = []; const now = Date.now(); let selected; if (select_channel_mode == 1) { selected = getState(root + "selected_channel")?.val; } else { selected = getState(root + "selected_channel_favorite")?.val; } if (!selected) return []; const base = data + selected; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für selected_channel " + selected + ": " + e, "warn"); return []; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const stop_ts = parseGermanDateTime(entry.stop); if (stop_ts > now) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon}); } } setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } schedule("0 3 * * *", async () => { await loadChannelsAndProgrammes(); await initSenderEditList(); }); schedule("1 * * * * *", async () => { if (loading === true) return; const mode = getState(root + "broadcast_mode")?.val; if (mode === "selected") { await updateSelectedChannelBroadcast(); } else { await updateAllActiveBroadcasts(); } }); function registerTriggers() { on({ id: root + "refresh", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { await loadChannelsAndProgrammes(); await createSenderList("active"); await createSenderList("favorite"); await initSenderEditList(); await updateMaxOrderNumber(); } catch (err) {} setState(root + "refresh", false, true); } }); on({ id: root + "edit.save", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { if (select_channel_mode == 1) { await saveEditToSender(getState(root + "selected_channel").val); } else if (select_channel_mode == 2) { await saveEditToSender(getState(root + "selected_channel_favorite").val); } await createSenderList("active"); await createSenderList("favorite"); } catch (err) {} setState(root + "edit.save", false, true); } }); on({ id: root + "selected_channel", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 1; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); on({ id: root + "selected_channel_favorite", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 2; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); const TYPE_FILTERS = [root + "filter_favorite", root + "filter_free", root + "filter_pay", root + "filter_typ_film", root + "filter_typ_serie", root + "filter_typ_doku", root + "filter_typ_kinder", root + "filter_typ_musik", root + "filter_typ_shopping", root + "filter_typ_sport", root + "filter_typ_nachrichten", root + "filter_typ_vollprogramm"]; on({ id: TYPE_FILTERS, change: "ne" }, async () => { await updateAllActiveBroadcasts(); }); on({ id: root + "broadcast_mode", change: "ne" }, async obj => { const val = obj.state.val; if (val === 'all') { await updateAllActiveBroadcasts(); } }); on({ id: root + "sender_edit_list", change: "ne" }, async obj => { if (!obj || !obj.state || !obj.state.val) return; let list; try { list = JSON.parse(obj.state.val); } catch (e) { log("Fehler: sender_edit_list enthält kein gültiges JSON", "warn"); return; } for (const entry of list) { const id = entry.value; const base = data + id; setStateIfChanged(base + ".order", Number(entry.order || 0)); setStateIfChanged(base + ".favorite", entry.favorite === true); setStateIfChanged(base + ".active", entry.active === true); } await initSenderEditList(); }); }Viel Spaß und Erfolg bei der Umsetzung.
Ro75.
Änderung 1.0.1 - 24.06.2026
- Start und Stop jetzt nur im klassischen Stil (HH:MM). Also 19:15, 20:15, etc.
- Weiterer Datenpunkt "selected_channel_favorite" löst die selben Ereignisse wie "selected_channel" aus. Senderdetails für Visualisierung und in der Hauptanzeige alle Sendungen zum gewählten Kanal.
- Zentrale Sendereditierung. Jetzt können über den Datenpunkt "sender_edit_list" die wichtigsten Einstellungen pro Sender zentral vorgenommen werden. Dazu wird der Inhalt des Datenpunktes (json) angepasst. Dazu kann der Sender den Favoriten (favorite) hinzugefügt, aktiviert oder deaktiviert (active) werden. Darüber hinaus sit es möglich, die Senderreihenfolge (order) festzulegen. Wenn alle Änderungen durchgeführt - einfach speichern und ein Trigger erledigt automatisch den Rest (Daten werden an Sender übergeben und Einstellungen werden sofort aktiv).
-

Sobald in einem der beiden Auswahl-Felder ein Sender eingestellt wurde. werden die Daten in .edit.xxx kopiert. Die Taste Details öffnet eine View im Popup. Die Widget da drin greifen auf die .edit.xxx zu. Die Taste Speichern triggert einen Datenpunkt (save) der das Rückschreiben auslöst.

Ro75.
-

Sobald in einem der beiden Auswahl-Felder ein Sender eingestellt wurde. werden die Daten in .edit.xxx kopiert. Die Taste Details öffnet eine View im Popup. Die Widget da drin greifen auf die .edit.xxx zu. Die Taste Speichern triggert einen Datenpunkt (save) der das Rückschreiben auslöst.

Ro75.
-
Siehe Post #11.
Und hier die vom Popup.
[{"tpl":"tplVis-materialdesign-CheckBox","data":{"oid":"0_userdata.0.epg.edit.active","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","toggleType":"boolean","stateIfNotTrueValue":"on","vibrateOnMobilDevices":"50","labelPosition":"right","labelClickActive":true,"valueFontFamily":"InterVariable","valueFontSize":"13","colorCheckBox":"rgba(110, 70, 62, 0.45)","colorCheckBoxBorder":"rgba(110, 70, 62, 1)","colorCheckBoxHover":"#mdwTheme:vis-materialdesign.0.colors.checkbox.hover","labelColorFalse":"rgba(255,255,255,0.8)","labelColorTrue":"rgba(255,255,255,0.8)","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"valueOff":"false","valueOn":"true","labelFalse":"Aktiv","labelTrue":"Aktiv","g_lock":false,"autoLockAfter":"10","lockIconTop":"5","lockIconLeft":"5","lockIconColor":"#mdwTheme:vis-materialdesign.0.colors.checkbox.lock_icon","lockFilterGrayscale":"30","clickSoundVolume":"0.5"},"style":{"left":"413px","top":"40px","width":"108px","height":"30px","z-index":"999","text-shadow":"3px 3px 4px #000","font-family":"InterVariable","font-style":"normal","font-variant":"normal","font-weight":"normal","font-size":"12.5px"},"widgetSet":"materialdesign"},{"tpl":"tplImage","data":{"g_fixed":true,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","refreshInterval":"0","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"src":"/vis.0/dashboard/struktur.jpg","stretch":true,"name":"Background","locked":true},"style":{"left":"0px","top":"0px","width":"780px","height":"350px","z-index":"1"},"widgetSet":"basic"},{"tpl":"tplValueStringImg","data":{"oid":"nothing_selected","g_fixed":false,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","refreshInterval":"0","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0},"style":{"left":"99px","top":"86px"},"widgetSet":"basic"},{"tpl":"tplValueStringImg","data":{"oid":"","g_fixed":false,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","refreshInterval":"0","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0},"style":{"left":"10px","top":"14px","width":"300px","height":"300px","z-index":"2"},"widgetSet":"basic"},{"tpl":"tplVis-materialdesign-CheckBox","data":{"oid":"0_userdata.0.epg.edit.favorite","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","toggleType":"boolean","stateIfNotTrueValue":"on","vibrateOnMobilDevices":"50","labelPosition":"right","labelClickActive":true,"valueFontFamily":"InterVariable","valueFontSize":"13","colorCheckBox":"rgba(110, 70, 62, 0.45)","colorCheckBoxBorder":"rgba(110, 70, 62, 1)","colorCheckBoxHover":"#mdwTheme:vis-materialdesign.0.colors.checkbox.hover","labelColorFalse":"rgba(255,255,255,0.8)","labelColorTrue":"rgba(255,255,255,0.8)","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"valueOff":"false","valueOn":"true","labelFalse":"Favorit","labelTrue":"Favorit","g_lock":false,"autoLockAfter":"10","lockIconTop":"5","lockIconLeft":"5","lockIconColor":"#mdwTheme:vis-materialdesign.0.colors.checkbox.lock_icon","lockFilterGrayscale":"30","clickSoundVolume":"0.5"},"style":{"left":"412px","top":"69px","width":"108px","height":"30px","z-index":"999","text-shadow":"3px 3px 4px #000","font-family":"InterVariable","font-style":"normal","font-variant":"normal","font-weight":"normal","font-size":"12.5px"},"widgetSet":"materialdesign"},{"tpl":"tplVis-materialdesign-Button-Adition","data":{"oid":"0_userdata.0.epg.edit.order","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","buttonStyle":"raised","vibrateOnMobilDevices":"50","clickSoundVolume":"0.5","buttontext":" Plus","textFontFamily":"#mdwTheme:vis-materialdesign.0.fonts.button.default.text","textFontSize":"#mdwTheme:vis-materialdesign.0.fontSizes.button.default.text","mdwButtonPrimaryColor":"var(--materialdesign-widget-theme-color-button-default-primary)","mdwButtonSecondaryColor":"var(--materialdesign-widget-theme-color-button-default-secondary)","image":"plus","iconPosition":"left","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"minmax":"100","value":"1"},"style":{"left":"544px","top":"52px","width":"101px","height":"40px","z-index":"3","color":"#ffffff"},"widgetSet":"materialdesign"},{"tpl":"tplVis-materialdesign-Button-Adition","data":{"oid":"0_userdata.0.epg.edit.order","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","buttonStyle":"raised","vibrateOnMobilDevices":"50","clickSoundVolume":"0.5","buttontext":" Minus","textFontFamily":"#mdwTheme:vis-materialdesign.0.fonts.button.default.text","textFontSize":"#mdwTheme:vis-materialdesign.0.fontSizes.button.default.text","mdwButtonPrimaryColor":"var(--materialdesign-widget-theme-color-button-default-primary)","mdwButtonSecondaryColor":"var(--materialdesign-widget-theme-color-button-default-secondary)","image":"minus","iconPosition":"left","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"minmax":"{0_userdata.0.epg.max_order_number}","value":"-1"},"style":{"left":"657px","top":"52px","width":"101px","height":"40px","z-index":"3","color":"#ffffff"},"widgetSet":"materialdesign"},{"tpl":"tplValueFloat","data":{"oid":"0_userdata.0.epg.edit.order","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","is_comma":"true","factor":"1","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"html_prepend":"Reihenfolge: "},"style":{"left":"544px","top":"21px","z-index":"3","color":"#ffffff","text-align":"center","text-shadow":"3px 3px 4px #000","font-family":"InterVariable","font-style":"normal","font-variant":"normal","font-weight":"normal","font-size":"medium","width":"215px","height":"22px"},"widgetSet":"basic"},{"tpl":"tplValueFloat","data":{"oid":"0_userdata.0.epg.max_order_number","g_fixed":false,"g_visibility":false,"g_css_font_text":true,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","is_comma":"true","factor":"1","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"html_prepend":"höchste Position: "},"style":{"left":"543px","top":"101px","z-index":"3","color":"#ffffff","text-align":"center","text-shadow":"3px 3px 4px #000","font-family":"InterVariable","font-style":"normal","font-variant":"normal","font-weight":"normal","font-size":"medium","width":"215px","height":"22px"},"widgetSet":"basic"},{"tpl":"tplJquiButtonState","data":{"oid":"0_userdata.0.epg.edit.save","g_fixed":false,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","buttontext":"Speichern","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"value":"true"},"style":{"left":"584px","top":"275px","width":"174px","height":"53px","z-index":"3"},"widgetSet":"jqui"},{"tpl":"tplValueStringImg","data":{"oid":"0_userdata.0.epg.edit.icon_url","g_fixed":false,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":false,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","refreshInterval":"0","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0},"style":{"left":"12px","top":"12px","z-index":"3","width":"331px","height":"331px"},"widgetSet":"basic"}]Ro75.
-
Hallo. Heute mal einen kleinen Einblick in mein neustes Projekt, mit etwas KI Unterstützung. Knapp 700 Zeilen Code liefern das hier:


Ca. 170 Sender. Favoritensteuerung, Senderreihenfolge, automatische Aktualisierung.

Oder alle anstehenden Sendungen zu einem Sender.EPG-TV für ioBroker
Mit diesem Script lassen sich aktuelle TV-Programmdaten (EPG) von über 160 deutschen TV-Sendern direkt in ioBroker integrieren. Die Daten werden automatisch aus einer XMLTV-Quelle geladen und als Datenpunkte zur Verfügung gestellt.
Neben den eigentlichen Programminformationen bietet das Script eine komfortable Senderverwaltung mit Favoritenfunktion, individueller Sortierung sowie umfangreichen Filtermöglichkeiten. Dadurch eignet es sich hervorragend für Visualisierungen in Jarvis, VIS oder anderen Dashboards.
Funktionen
- Über 160 deutsche TV-Sender
- Free-TV und Pay-TV
- Automatischer Import der EPG-Daten
- Aktuelle und kommende Sendungen
- Senderlogos
- Favoritenverwaltung
- Individuelle Senderreihenfolge
- Filter nach Senderart und Kategorie
- Automatische tägliche Aktualisierung
Voraussetzungen
Für das Script werden folgende zusätzliche NPM-Module benötigt:
Modul Verwendung xml2jsEinlesen und Verarbeiten der XMLTV-Daten user-agentsErzeugung eines Browser-User-Agents für den Abruf der EPG-Daten Die Module können im JavaScript-Adapter unter „Zusätzliche NPM-Module“ eingetragen werden. Anschließend den JavaScript-Adapter neu starten.
Visualisierung
Die Ausgabe erfolgt hauptsächlich über den Datenpunkt
broadcast_list.Für die Darstellung verwende ich das JSON-Widget aus VIS Material Design ab Version 0.5.94. Die Programmdaten werden bereits als fertiges JSON bereitgestellt und können direkt im Widget verarbeitet werden.
Wichtige Hinweise
Beim ersten Start werden automatisch alle verfügbaren Sender aus der XMLTV-Quelle eingelesen und die benötigten Datenpunkte angelegt.
Folgende Datenpunkte werden vollständig vom Script verwaltet und sollten nicht manuell geändert werden:
sender_jsonactive_sender_listfavorite_sender_listbroadcast_list
Die Senderbearbeitung erfolgt ausschließlich über die dafür vorgesehenen Datenpunkte im Bereich
edit.*.Wichtige Datenpunkte
Datenpunkt Bedeutung Verwendung sender_jsonInterne Senderliste Wird vom Script zur Senderverwaltung verwendet active_sender_listAlle aktiven Sender Für Dropdowns und Senderauswahl favorite_sender_listAlle Favoritensender Für Favoritenlisten und Senderauswahl broadcast_listProgrammausgabe Hauptdatenpunkt für Tabellen, Listen und TV-Guides selected_channelAusgewählter Sender Zeigt aktuelle und kommende Sendungen eines Senders refreshManueller Reload Lädt Sender- und EPG-Daten neu filter_*Filterfunktionen Filterung nach Favoriten, Free-/Pay-TV und Kategorien broadcast_modeAnzeigemodus all= alle Sender,selected= ausgewählter SendercountAnzahl gefundener Sender Kontrollwert nach dem Einlesen max_order_numberHöchste vergebene Sendernummer Hilfswert für die Senderreihenfolge Verfügbare Filter
- Favoriten
- Free-TV
- Pay-TV
- Film
- Serie
- Doku
- Kinder
- Musik
- Shopping
- Sport
- Nachrichten
- Vollprogramm
Automatische Aktualisierung
- Täglicher EPG-Import um 03:00 Uhr
- Manueller Reload über
refresh - Laufende Aktualisierung der Programmliste
Verwendung
Alle aktuellen Sendungen anzeigen
broadcast_mode = allZeigt die aktuell laufenden Sendungen aller aktiven Sender in
broadcast_list.Einzelnen Sender anzeigen
selected_channelauf die gewünschte Sender-ID setzen.Anschließend enthält
broadcast_listdie aktuelle sowie alle kommenden Sendungen dieses Senders.Favoriten verwenden
Sender können als Favoriten markiert und über
favorite_sender_listoder den Filterfilter_favoriteverwendet werden.Sender filtern
Die Ausgabe kann über die verschiedenen
filter_*Datenpunkte nach Senderart oder Kategorie eingeschränkt werden.Hinweis
Dieses Script wurde mit Unterstützung von KI entwickelt und anschließend in ioBroker getestet und angepasst. Trotz umfangreicher Tests können Fehler oder Verbesserungspotenziale nicht ausgeschlossen werden. Rückmeldungen, Fehlerberichte und Verbesserungsvorschläge sind daher jederzeit willkommen.Hier das Script:
// === EPG - TV-Daten === //Erst-Version 1.0.0 - 22.06.2026 //Version 1.0.1 - 24.06.2026 //Ersteller Ro75. //Voraussetzungen (Version 1.0.1 getestet mit) //NodeJS: 22.22.x //Javascript-Adapter: 9.0.18 //Admin-Adapter: 7.8.23 //js-controler: 7.2.2 //********** START KONFIGURATION ********** const root = "0_userdata.0.epg."; const data = root + "data."; const sender = root + "sender_json"; //********** AB HIER NICHTS MEHR ÄNDERN ********** const xml2js = require("xml2js"); const UserAgent = require("user-agents"); let select_channel_mode = 0; let loading = false; const CONFIG = { "1": { url_id: "1-2-3tv.de", art: "Free", typ: "Shopping", name: "1-2-3.tv" }, "2": { url_id: "13THSTREET.de", art: "Pay", typ: "Film", name: "13th Street" }, "3": { url_id: "3sat.de", art: "Free", typ: "Vollprogramm", name: "3sat" }, "4": { url_id: "atv.de", art: "Free", typ: "Vollprogramm", name: "ATV" }, "5": { url_id: "AnimalPlanet.de", art: "Pay", typ: "Doku", name: "Animal Planet" }, "6": { url_id: "ANIXESerie.de", art: "Free", typ: "Serie", name: "ANIXE Serie" }, "7": { url_id: "ANIXE+.de", art: "Free", typ: "Vollprogramm", name: "ANIXE+" }, "8": { url_id: "ARD-alpha.de", art: "Free", typ: "Doku", name: "ARD alpha" }, "9": { url_id: "ARTE.de", art: "Free", typ: "Doku", name: "ARTE" }, "10": { url_id: "automotorundsport.de", art: "Pay", typ: "Doku", name: "auto motor und sport" }, "11": { url_id: "AXNBlack.de", art: "Pay", typ: "Film", name: "AXN Black" }, "12": { url_id: "AXNWhite.de", art: "Pay", typ: "Serie", name: "AXN White" }, "13": { url_id: "Beate-UhseTV.de", art: "Pay", typ: "Film", name: "Beate-Uhse.TV" }, "14": { url_id: "BR.de", art: "Free", typ: "Vollprogramm", name: "BR Fernsehen" }, "15": { url_id: "CartoonNetwork.de", art: "Pay", typ: "Kinder", name: "Cartoon Network" }, "16": { url_id: "Cartoonito.de", art: "Pay", typ: "Kinder", name: "Cartoonito" }, "17": { url_id: "ComedyCentral.de", art: "Free", typ: "Serie", name: "Comedy Central" }, "18": { url_id: "Crime+Investigation.de", art: "Pay", typ: "Doku", name: "Crime + Investigation" }, "19": { url_id: "CuriosityChannel.de", art: "Pay", typ: "Doku", name: "Curiosity Channel" }, "20": { url_id: "DasErste.de", art: "Free", typ: "Vollprogramm", name: "Das Erste" }, "21": { url_id: "DAZN.de", art: "Pay", typ: "Sport", name: "DAZN" }, "22": { url_id: "DAZN1.de", art: "Pay", typ: "Sport", name: "DAZN 1" }, "23": { url_id: "DAZN2.de", art: "Pay", typ: "Sport", name: "DAZN 2" }, "24": { url_id: "DeluxeDanceByKontor.de", art: "Free", typ: "Musik", name: "DELUXE DANCE by Kontor" }, "25": { url_id: "DeluxeFlashback.de", art: "Free", typ: "Musik", name: "DELUXE FLASHBACK" }, "26": { url_id: "DeluxeLounge.de", art: "Free", typ: "Musik", name: "DELUXE LOUNGE" }, "27": { url_id: "DeluxeMusic.de", art: "Free", typ: "Musik", name: "DELUXE MUSIC" }, "28": { url_id: "DeluxeRap.de", art: "Free", typ: "Musik", name: "DELUXE RAP" }, "29": { url_id: "DeluxeRock.de", art: "Free", typ: "Musik", name: "DELUXE ROCK" }, "30": { url_id: "DeutschesMusikFernsehen.de",art: "Free", typ: "Musik", name: "Deutsches Musik Fernsehen" }, "31": { url_id: "Discovery.de", art: "Pay", typ: "Doku", name: "Discovery Channel" }, "32": { url_id: "DisneyChannel.de", art: "Free", typ: "Kinder", name: "Disney Channel" }, "33": { url_id: "DMAX.de", art: "Free", typ: "Doku", name: "DMAX" }, "34": { url_id: "dokusat.de", art: "Free", typ: "Doku", name: "dokuSAT" }, "35": { url_id: "EDGESport.de", art: "Pay", typ: "Sport", name: "EDGE Sport" }, "36": { url_id: "emsTV.de", art: "Free", typ: "Musik", name: "ems TV" }, "37": { url_id: "eSPORTS1.de", art: "Pay", typ: "Sport", name: "eSPORTS1" }, "38": { url_id: "Eurosport1.de", art: "Free", typ: "Sport", name: "Eurosport 1" }, "39": { url_id: "Eurosport2.de", art: "Pay", typ: "Sport", name: "Eurosport 2" }, "40": { url_id: "Heimatkanal.de", art: "Pay", typ: "Film", name: "Heimatkanal" }, "41": { url_id: "HGTV.de", art: "Free", typ: "Doku", name: "HGTV" }, "42": { url_id: "HISTORY.de", art: "Pay", typ: "Doku", name: "HISTORY" }, "43": { url_id: "hr.de", art: "Free", typ: "Vollprogramm", name: "hr Fernsehen" }, "44": { url_id: "HSE.de", art: "Free", typ: "Shopping", name: "HSE" }, "45": { url_id: "HSE24.de", art: "Free", typ: "Shopping", name: "HSE24" }, "46": { url_id: "Jukebox.de", art: "Free", typ: "Musik", name: "Jukebox" }, "47": { url_id: "JustCooking.de", art: "Free", typ: "Doku", name: "Just Cooking" }, "48": { url_id: "KabelEins.de", art: "Free", typ: "Vollprogramm", name: "kabel eins" }, "49": { url_id: "KabelEinsCLASSICS.de", art: "Pay", typ: "Film", name: "kabel eins CLASSICS" }, "50": { url_id: "KabelEinsDoku.de", art: "Free", typ: "Doku", name: "kabel eins Doku" }, "51": { url_id: "KiKA.de", art: "Free", typ: "Kinder", name: "KiKA" }, "52": { url_id: "KinoweltTV.de", art: "Pay", typ: "Film", name: "Kinowelt TV" }, "53": { url_id: "MDRSachsen.de", art: "Free", typ: "Vollprogramm", name: "MDR Sachsen" }, "54": { url_id: "MTV.de", art: "Pay", typ: "Musik", name: "MTV" }, "55": { url_id: "MTV00s.de", art: "Pay", typ: "Musik", name: "MTV 00s" }, "56": { url_id: "MTV80s.de", art: "Pay", typ: "Musik", name: "MTV 80s" }, "57": { url_id: "MTV90s.de", art: "Pay", typ: "Musik", name: "MTV 90s" }, "58": { url_id: "MTVHits.de", art: "Pay", typ: "Musik", name: "MTV Hits" }, "59": { url_id: "MTVLive.de", art: "Pay", typ: "Musik", name: "MTV Live" }, "60": { url_id: "n-tv.de", art: "Free", typ: "Doku", name: "n-tv" }, "61": { url_id: "N24Doku.de", art: "Free", typ: "Doku", name: "N24 Doku" }, "62": { url_id: "NationalGeographic.de", art: "Pay", typ: "Doku", name: "National Geographic" }, "63": { url_id: "NationalGeographicWILD.de", art: "Pay", typ: "Doku", name: "Nat Geo Wild" }, "64": { url_id: "NDR.de", art: "Free", typ: "Vollprogramm", name: "NDR Fernsehen" }, "65": { url_id: "Nick.de", art: "Free", typ: "Kinder", name: "Nick" }, "66": { url_id: "NickComedyCentral+1.de", art: "Free", typ: "Kinder", name: "Nick / CC +1" }, "67": { url_id: "NickJr.de", art: "Pay", typ: "Kinder", name: "Nick Jr." }, "68": { url_id: "Nicktoons.de", art: "Pay", typ: "Kinder", name: "Nicktoons" }, "69": { url_id: "NITRO.de", art: "Free", typ: "Vollprogramm", name: "NITRO" }, "70": { url_id: "ONE.de", art: "Free", typ: "Vollprogramm", name: "ONE" }, "71": { url_id: "phoenix.de", art: "Free", typ: "Doku", name: "phoenix" }, "72": { url_id: "ProSieben.de", art: "Free", typ: "Vollprogramm", name: "ProSieben" }, "73": { url_id: "ProSiebenFUN.de", art: "Pay", typ: "Serie", name: "ProSieben FUN" }, "74": { url_id: "ProSiebenMAXX.de", art: "Free", typ: "Vollprogramm", name: "ProSieben MAXX" }, "75": { url_id: "QVC.de", art: "Free", typ: "Shopping", name: "QVC" }, "76": { url_id: "rbbBrandenburg.de", art: "Free", typ: "Vollprogramm", name: "rbb Brandenburg" }, "77": { url_id: "RiC.de", art: "Free", typ: "Kinder", name: "RiC" }, "78": { url_id: "RomanceTV.de", art: "Pay", typ: "Film", name: "Romance TV" }, "79": { url_id: "RTL.de", art: "Free", typ: "Vollprogramm", name: "RTL" }, "80": { url_id: "RTLCrime.de", art: "Pay", typ: "Serie", name: "RTL Crime" }, "81": { url_id: "RTLLiving.de", art: "Pay", typ: "Doku", name: "RTL Living" }, "82": { url_id: "RTLPassion.de", art: "Pay", typ: "Serie", name: "RTL Passion" }, "83": { url_id: "RTLplus.de", art: "Free", typ: "Serie", name: "RTLplus" }, "84": { url_id: "RTLup.de", art: "Free", typ: "Vollprogramm", name: "RTLup" }, "85": { url_id: "RTLZWEI.de", art: "Free", typ: "Vollprogramm", name: "RTLZWEI" }, "86": { url_id: "SAT1.de", art: "Free", typ: "Vollprogramm", name: "SAT.1" }, "87": { url_id: "SAT1emotions.de", art: "Pay", typ: "Serie", name: "SAT.1 emotions" }, "88": { url_id: "SAT1GOLD.de", art: "Free", typ: "Vollprogramm", name: "SAT.1 GOLD" }, "89": { url_id: "SchlagerDeluxe.de", art: "Free", typ: "Musik", name: "Schlager Deluxe" }, "90": { url_id: "SchlagerparadiesTV.de", art: "Free", typ: "Musik", name: "Schlagerparadies TV" }, "91": { url_id: "sixx.de", art: "Free", typ: "Vollprogramm", name: "sixx" }, "92": { url_id: "SkyCinemaAction.de", art: "Pay", typ: "Film", name: "Sky Cinema Action" }, "93": { url_id: "SkyCinemaBlockbuster.de", art: "Pay", typ: "Film", name: "Sky Cinema Blockbuster" }, "94": { url_id: "SkyCinemaClassics.de", art: "Pay", typ: "Film", name: "Sky Cinema Classics" }, "96": { url_id: "SkyCinemaPremiere.de", art: "Pay", typ: "Film", name: "Sky Cinema Premiere" }, "97": { url_id: "SkyCrime.de", art: "Pay", typ: "Serie", name: "Sky Crime" }, "98": { url_id: "SkyDocumentaries.de", art: "Pay", typ: "Doku", name: "Sky Documentaries" }, "99": { url_id: "SkyNature.de", art: "Pay", typ: "Doku", name: "Sky Nature" }, "100": { url_id: "SkyOne.de", art: "Pay", typ: "Vollprogramm", name: "Sky One" }, "101": { url_id: "SkySciFi.de", art: "Pay", typ: "Film", name: "Sky Sci-Fi" }, "102": { url_id: "SkyShowcase.de", art: "Pay", typ: "Vollprogramm", name: "Sky Showcase" }, "103": { url_id: "SkySport1.de", art: "Pay", typ: "Sport", name: "Sky Sport 1" }, "104": { url_id: "SkySport10.de", art: "Pay", typ: "Sport", name: "Sky Sport 10" }, "105": { url_id: "SkySport2.de", art: "Pay", typ: "Sport", name: "Sky Sport 2" }, "106": { url_id: "SkySport3.de", art: "Pay", typ: "Sport", name: "Sky Sport 3" }, "107": { url_id: "SkySport4.de", art: "Pay", typ: "Sport", name: "Sky Sport 4" }, "108": { url_id: "SkySport5.de", art: "Pay", typ: "Sport", name: "Sky Sport 5" }, "109": { url_id: "SkySport6.de", art: "Pay", typ: "Sport", name: "Sky Sport 6" }, "110": { url_id: "SkySport7.de", art: "Pay", typ: "Sport", name: "Sky Sport 7" }, "111": { url_id: "SkySport8.de", art: "Pay", typ: "Sport", name: "Sky Sport 8" }, "112": { url_id: "SkySport9.de", art: "Pay", typ: "Sport", name: "Sky Sport 9" }, "113": { url_id: "SkySportAustria1.de", art: "Pay", typ: "Sport", name: "Sky Sport Austria 1" }, "114": { url_id: "SkySportBundesliga.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga" }, "115": { url_id: "SkySportBundesliga1.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 1" }, "116": { url_id: "SkySportBundesliga10.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 10" }, "117": { url_id: "SkySportBundesliga2.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 2" }, "118": { url_id: "SkySportBundesliga3.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 3" }, "119": { url_id: "SkySportBundesliga4.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 4" }, "120": { url_id: "SkySportBundesliga5.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 5" }, "121": { url_id: "SkySportBundesliga6.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 6" }, "122": { url_id: "SkySportBundesliga7.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 7" }, "123": { url_id: "SkySportBundesliga8.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 8" }, "124": { url_id: "SkySportBundesliga9.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 9" }, "125": { url_id: "SkySportBundesligaUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga UHD" }, "126": { url_id: "SkySportF1.de", art: "Pay", typ: "Sport", name: "Sky Sport F1" }, "127": { url_id: "SkySportGolf.de", art: "Pay", typ: "Sport", name: "Sky Sport Golf" }, "128": { url_id: "SkySportKompakt1.de", art: "Pay", typ: "Sport", name: "Sky Sport Kompakt 1" }, "129": { url_id: "SkySportMix.de", art: "Pay", typ: "Sport", name: "Sky Sport Mix" }, "130": { url_id: "SkySportNews.de", art: "Pay", typ: "Sport", name: "Sky Sport News" }, "131": { url_id: "SkySportPremierLeague.de", art: "Pay", typ: "Sport", name: "Sky Sport Premier League" }, "132": { url_id: "SkySportTennis.de", art: "Pay", typ: "Sport", name: "Sky Sport Tennis" }, "133": { url_id: "SkySportTopEvent.de", art: "Pay", typ: "Sport", name: "Sky Sport Top Event" }, "134": { url_id: "SkySportUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport UHD" }, "135": { url_id: "SkyAtlantic.de", art: "Pay", typ: "Serie", name: "Sky Atlantic" }, "136": { url_id: "SkyKrimi.de", art: "Pay", typ: "Serie", name: "Sky Krimi" }, "137": { url_id: "sonnenklarTV.de", art: "Free", typ: "Shopping", name: "sonnenklar.TV" }, "138": { url_id: "SpiegelGeschichte.de", art: "Pay", typ: "Doku", name: "Spiegel Geschichte" }, "139": { url_id: "SPORT1.de", art: "Free", typ: "Sport", name: "SPORT1" }, "140": { url_id: "SPORT1+.de", art: "Pay", typ: "Sport", name: "SPORT1+" }, "141": { url_id: "SRFernsehen.de", art: "Free", typ: "Vollprogramm", name: "SR Fernsehen" }, "142": { url_id: "SuperRTL.de", art: "Free", typ: "Kinder", name: "SUPER RTL" }, "143": { url_id: "SWRBW.de", art: "Free", typ: "Vollprogramm", name: "SWR BW" }, "144": { url_id: "tagesschau24.de", art: "Free", typ: "Doku", name: "tagesschau24" }, "145": { url_id: "Tele5.de", art: "Free", typ: "Film", name: "Tele 5" }, "146": { url_id: "TLC.de", art: "Free", typ: "Doku", name: "TLC" }, "147": { url_id: "TOGGOplus.de", art: "Free", typ: "Kinder", name: "TOGGO plus" }, "148": { url_id: "UniversalTV.de", art: "Pay", typ: "Serie", name: "Universal TV" }, "149": { url_id: "VolksmusikTV.de", art: "Free", typ: "Musik", name: "Volksmusik TV" }, "150": { url_id: "VOX.de", art: "Free", typ: "Vollprogramm", name: "VOX" }, "151": { url_id: "VOXup.de", art: "Free", typ: "Vollprogramm", name: "VOXup" }, "152": { url_id: "WaidwerkTV.de", art: "Pay", typ: "Doku", name: "Waidwerk TV" }, "153": { url_id: "WarnerTVComedy.de", art: "Pay", typ: "Serie", name: "Warner TV Comedy" }, "154": { url_id: "WarnerTVFilm.de", art: "Pay", typ: "Film", name: "Warner TV Film" }, "155": { url_id: "WarnerTVSerie.de", art: "Pay", typ: "Serie", name: "Warner TV Serie" }, "156": { url_id: "WDR.de", art: "Free", typ: "Vollprogramm", name: "WDR Fernsehen" }, "157": { url_id: "WELT.de", art: "Free", typ: "Doku", name: "WELT" }, "158": { url_id: "WeltderWunder.de", art: "Free", typ: "Doku", name: "Welt der Wunder" }, "159": { url_id: "ZDF.de", art: "Free", typ: "Vollprogramm", name: "ZDF" }, "160": { url_id: "zdf_neo.de", art: "Free", typ: "Vollprogramm", name: "ZDFneo" }, "161": { url_id: "ZDFinfo.de", art: "Free", typ: "Doku", name: "ZDFinfo" } }; async function smartCreateState(id, value, options = {}) { if (existsState(id)) return; await createState(id, value, options); } function sleepMs(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const MAP_URL_TO_ID = {}; for (const key in CONFIG) MAP_URL_TO_ID[CONFIG[key].url_id] = Number(key); function parseXmltvTime(t) { const [stamp, offset] = t.split(" "); const d = new Date(`${stamp.slice(0,4)}-${stamp.slice(4,6)}-${stamp.slice(6,8)}T${stamp.slice(8,10)}:${stamp.slice(10,12)}:${stamp.slice(12,14)}${offset}`); return `${String(d.getDate()).padStart(2,"0")}.${String(d.getMonth()+1).padStart(2,"0")}.${d.getFullYear()} ` + `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}`; } function parseGermanDateTime(str) { const [datePart, timePart] = str.split(" "); const [day, month, year] = datePart.split(".").map(Number); const [hour, minute, second] = timePart.split(":").map(Number); return new Date(year, month - 1, day, hour, minute, second).getTime(); } function setStateIfChanged(id, value, STOPPED = false) { if (STOPPED) return; const state = getState(id); if (!state) return; const old = state.val; const isObject = v => v !== null && typeof v === "object"; if (isObject(value)) { const newStr = JSON.stringify(value); let oldStr = null; if (typeof old === "string") oldStr = old; else if (isObject(old)) oldStr = JSON.stringify(old); if (newStr === oldStr) return; return setState(id, newStr, true); } if (old === value) return; setState(id, value, true); } async function main() { loading = true; await smartCreateState(sender, "[]", {name: "Senderliste", type: "string", read: true, write: false}); await smartCreateState(root + "count", 0, {name: "Anzahl", type: "number", read: true, write: false}); await smartCreateState(root + "refresh", false, {name: "Aktualisieren", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_favorite", false, {name: "Filter nur Favoriten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_free", false, {name: "Filter nur Free-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_pay", false, {name: "Filter nur Pay-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_film", false, {name: "Filter Typ Film", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_serie", false, {name: "Filter Typ Serie", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_doku", false, {name: "Filter Typ Doku", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_kinder", false, {name: "Filter Typ Kinder", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_musik", false, {name: "Filter Typ Musik", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_shopping", false, {name: "Filter Typ Shopping", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_sport", false, {name: "Filter Typ Sport", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_nachrichten", false, {name: "Filter Typ Nachrichten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_vollprogramm", false, {name: "Filter Typ Vollprogramm", type: "boolean", read: true, write: true}); await smartCreateState(root + "broadcast_mode", "all", {name: "Broadcast Modus", type: "string", read: true, write: true}); await smartCreateState(root + "broadcast_list", "[]", {name: "aktuelle Sendungen", type: "string", read: true, write: false}); await smartCreateState(root + "selected_channel", 0, {name: "Ausgewählter Sender", type: "number", read: true, write: true}); await smartCreateState(root + "selected_channel_favorite", 0, {name: "Ausgewählter Favorit-Sender", type: "number", read: true, write: true}); await smartCreateState(root + "active_sender_list", "[]", {name: "alle aktiven Sender mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "favorite_sender_list", "[]", {name: "alle Favoriten mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "max_order_number", 0, {name: "Höchste vergebene Sendernummer", type: "number", read: true, write: false}); await smartCreateState(root + "sender_edit_list", "[]", {name: "Editierungsliste", type: "string", read: true, write: true}); await smartCreateState(root + "edit.active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.icon_url", "", { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(root + "edit.art", "", { name: "Art", type: "string", read: true, write: false }); await smartCreateState(root + "edit.typ", "", { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(root + "edit.name", "", { name: "Name", type: "string", read: true, write: true }); await smartCreateState(root + "edit.order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); await smartCreateState(root + "edit.save", false, { name: "Speichern", type: "boolean", read: true, write: true }); await sleepMs(200); await loadChannelsAndProgrammes(); await sleepMs(500); await createSenderList("active"); await createSenderList("favorite"); await sleepMs(200); await initSenderEditList(); await sleepMs(2000); await updateAllActiveBroadcasts(); registerTriggers(); loading = false; } main(); function senderPassesFilters(internal_id) { const base = data + internal_id; const active = getState(base + ".active")?.val; if (!active) return false; const favoriteOnly = getState(root + "filter_favorite")?.val === true; if (favoriteOnly) { const fav = getState(base + ".favorite")?.val; if (!fav) return false; } const allowedArt = []; if (getState(root + "filter_free")?.val === true) allowedArt.push("Free"); if (getState(root + "filter_pay")?.val === true) allowedArt.push("Pay"); const art = getState(base + ".art")?.val; if (allowedArt.length > 0 && !allowedArt.includes(art)) return false; const senderTyp = getState(base + ".typ")?.val; const allowedTypes = []; if (getState(root + "filter_typ_film")?.val === true) allowedTypes.push("Film"); if (getState(root + "filter_typ_serie")?.val === true) allowedTypes.push("Serie"); if (getState(root + "filter_typ_doku")?.val === true) allowedTypes.push("Doku"); if (getState(root + "filter_typ_kinder")?.val === true) allowedTypes.push("Kinder"); if (getState(root + "filter_typ_musik")?.val === true) allowedTypes.push("Musik"); if (getState(root + "filter_typ_shopping")?.val === true) allowedTypes.push("Shopping"); if (getState(root + "filter_typ_sport")?.val === true) allowedTypes.push("Sport"); if (getState(root + "filter_typ_nachrichten")?.val === true) allowedTypes.push("Nachrichten"); if (getState(root + "filter_typ_vollprogramm")?.val === true) allowedTypes.push("Vollprogramm"); if (allowedTypes.length > 0 && !allowedTypes.includes(senderTyp)) return false; return true; } async function updateMaxOrderNumber() { const senderList = JSON.parse(getState(sender).val || "[]"); let max = 0; for (const s of senderList) { const order = getState(data + s.internal_id + ".order")?.val || 0; if (order > max) max = order; } setStateIfChanged(root + "max_order_number", max); return max; } async function loadSenderIntoEdit(id) { const base = data + id; setStateIfChanged(root + "edit.active", getState(base + ".active")?.val || false); setStateIfChanged(root + "edit.favorite", getState(base + ".favorite")?.val || false); setStateIfChanged(root + "edit.icon_url", getState(base + ".icon_url")?.val || ""); setStateIfChanged(root + "edit.art", getState(base + ".art")?.val || ""); setStateIfChanged(root + "edit.typ", getState(base + ".typ")?.val || ""); setStateIfChanged(root + "edit.name", getState(base + ".name")?.val || ""); setStateIfChanged(root + "edit.order", getState(base + ".order")?.val || 0); } async function saveEditToSender(id) { const base = data + id; setStateIfChanged(base + ".active", getState(root + "edit.active")?.val); setStateIfChanged(base + ".favorite", getState(root + "edit.favorite")?.val); setStateIfChanged(base + ".order", getState(root + "edit.order")?.val); } async function createEditSenderList() { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; const name = getState(base + ".name")?.val || ""; const order = Number(getState(base + ".order")?.val || 0); const favorite = getState(base + ".favorite")?.val === true; const active = getState(base + ".active")?.val === true; result.push({ text: name, value: s.internal_id, order: order, favorite: favorite, active: active }); } result.sort((a, b) => { const oa = a.order; const ob = b.order; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.text.localeCompare(b.text); }); return result; } async function initSenderEditList() { const list = await createEditSenderList(); setStateIfChanged(root + "sender_edit_list", JSON.stringify(list)); } async function loadChannelsAndProgrammes() { try { const ua = new UserAgent().toString(); const response = await httpGetAsync("https://iptv-epg.org/files/epg-de.xml", { timeout: 20000, headers: { "User-Agent": ua } }); if (!response || response.statusCode !== 200) return; const xml = response.data; const result = await xml2js.parseStringPromise(xml); const channels = result.tv.channel .filter(ch => MAP_URL_TO_ID[ch.$.id]) .map(ch => ({ id: ch.$.id, name: Array.isArray(ch["display-name"]) ? (typeof ch["display-name"][0] === "string" ? ch["display-name"][0] : ch["display-name"][0]._ || "") : "", internal_id: MAP_URL_TO_ID[ch.$.id], icon_url: ch.icon?.[0]?.$.src || "" })) .sort((a, b) => a.internal_id - b.internal_id); setStateIfChanged(root + "count", channels.length); setStateIfChanged(sender, JSON.stringify(channels.map(c => ({id: c.id, name: CONFIG[c.internal_id].name, internal_id: c.internal_id})))); const programmes = result.tv.programme .filter(p => MAP_URL_TO_ID[p.$.channel]) .map(p => ({ internal_id: MAP_URL_TO_ID[p.$.channel], start: parseXmltvTime(p.$.start), stop: parseXmltvTime(p.$.stop), title: Array.isArray(p.title) ? (p.title[0]._ || p.title[0]) : "", desc: Array.isArray(p.desc) ? (p.desc[0]._ || p.desc[0]) : "", date: Array.isArray(p.date) ? p.date[0] : "", episode: Array.isArray(p["episode-num"]) ? p["episode-num"][0] : "", categories: Array.isArray(p.category) ? p.category.map(c => (typeof c === "string" ? c : c._ || "")) : [], icon: p.icon?.[0]?.$.src || "" })); const grouped = {}; for (const p of programmes) { if (!grouped[p.internal_id]) grouped[p.internal_id] = []; grouped[p.internal_id].push(p); } for (const ch of channels) { const list = grouped[ch.internal_id] || []; const base = data + ch.internal_id; const now = Date.now(); const filtered = list.filter(p => { const stopTs = new Date( p.stop.slice(6,10) + "-" + p.stop.slice(3,5) + "-" + p.stop.slice(0,2) + "T" + p.stop.slice(11,19) ).getTime(); return stopTs >= now; }); // internal_id aus JSON entfernen const cleaned = filtered.map(p => { const { internal_id, ...rest } = p; return rest; }); await smartCreateState(base + ".broadcast", "[]", { name: "Sendungen", type: "string", read: true, write: false }); await smartCreateState(base + ".active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(base + ".favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(base + ".icon_url", ch.icon_url, { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(base + ".art", CONFIG[ch.internal_id].art, { name: "Art", type: "string", read: true, write: false }); await smartCreateState(base + ".typ", CONFIG[ch.internal_id].typ, { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(base + ".name", CONFIG[ch.internal_id].name, { name: "Name", type: "string", read: true, write: true }); await smartCreateState(base + ".order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); setStateIfChanged(base + ".broadcast", JSON.stringify(cleaned)); } } catch (err) { log(err, "error"); } } async function createSenderList(mode) { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; if (mode === "favorite") { const favorite = getState(base + ".favorite")?.val; if (!favorite) continue; } const senderName = getState(base + ".name")?.val || ""; result.push({text: senderName, value: s.internal_id }); } if (mode === "favorite") { result.sort((a, b) => { const orderA = getState(data + a.value + ".order")?.val || 0; const orderB = getState(data + b.value + ".order")?.val || 0; if (orderA > 0 && orderB === 0) return -1; if (orderA === 0 && orderB > 0) return 1; if (orderA > 0 && orderB > 0) return orderA - orderB; return a.text.localeCompare(b.text); }); } else { result.sort((a, b) => a.text.localeCompare(b.text)); } if (mode === "active") { setStateIfChanged(root + "active_sender_list", JSON.stringify(result)); } else if (mode === "favorite") { setStateIfChanged(root + "favorite_sender_list", JSON.stringify(result)); } return result; } async function updateAllActiveBroadcasts() { const result = []; const now = Date.now(); const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { if (!senderPassesFilters(s.internal_id)) continue; const base = data + s.internal_id; const order = getState(base + ".order")?.val || 0; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für Sender " + s.internal_id + ": " + e, "warn"); continue; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const start_ts = parseGermanDateTime(entry.start); const stop_ts = parseGermanDateTime(entry.stop); if (now >= start_ts && now < stop_ts) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon, order: order }); break; } } } result.sort((a, b) => { const oa = a.order || 0; const ob = b.order || 0; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.sender_name.localeCompare(b.sender_name); }); setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } async function updateSelectedChannelBroadcast() { const result = []; const now = Date.now(); let selected; if (select_channel_mode == 1) { selected = getState(root + "selected_channel")?.val; } else { selected = getState(root + "selected_channel_favorite")?.val; } if (!selected) return []; const base = data + selected; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für selected_channel " + selected + ": " + e, "warn"); return []; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const stop_ts = parseGermanDateTime(entry.stop); if (stop_ts > now) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon}); } } setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } schedule("0 3 * * *", async () => { await loadChannelsAndProgrammes(); await initSenderEditList(); }); schedule("1 * * * * *", async () => { if (loading === true) return; const mode = getState(root + "broadcast_mode")?.val; if (mode === "selected") { await updateSelectedChannelBroadcast(); } else { await updateAllActiveBroadcasts(); } }); function registerTriggers() { on({ id: root + "refresh", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { await loadChannelsAndProgrammes(); await createSenderList("active"); await createSenderList("favorite"); await initSenderEditList(); await updateMaxOrderNumber(); } catch (err) {} setState(root + "refresh", false, true); } }); on({ id: root + "edit.save", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { if (select_channel_mode == 1) { await saveEditToSender(getState(root + "selected_channel").val); } else if (select_channel_mode == 2) { await saveEditToSender(getState(root + "selected_channel_favorite").val); } await createSenderList("active"); await createSenderList("favorite"); } catch (err) {} setState(root + "edit.save", false, true); } }); on({ id: root + "selected_channel", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 1; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); on({ id: root + "selected_channel_favorite", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 2; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); const TYPE_FILTERS = [root + "filter_favorite", root + "filter_free", root + "filter_pay", root + "filter_typ_film", root + "filter_typ_serie", root + "filter_typ_doku", root + "filter_typ_kinder", root + "filter_typ_musik", root + "filter_typ_shopping", root + "filter_typ_sport", root + "filter_typ_nachrichten", root + "filter_typ_vollprogramm"]; on({ id: TYPE_FILTERS, change: "ne" }, async () => { await updateAllActiveBroadcasts(); }); on({ id: root + "broadcast_mode", change: "ne" }, async obj => { const val = obj.state.val; if (val === 'all') { await updateAllActiveBroadcasts(); } }); on({ id: root + "sender_edit_list", change: "ne" }, async obj => { if (!obj || !obj.state || !obj.state.val) return; let list; try { list = JSON.parse(obj.state.val); } catch (e) { log("Fehler: sender_edit_list enthält kein gültiges JSON", "warn"); return; } for (const entry of list) { const id = entry.value; const base = data + id; setStateIfChanged(base + ".order", Number(entry.order || 0)); setStateIfChanged(base + ".favorite", entry.favorite === true); setStateIfChanged(base + ".active", entry.active === true); } await initSenderEditList(); }); }Viel Spaß und Erfolg bei der Umsetzung.
Ro75.
Änderung 1.0.1 - 24.06.2026
- Start und Stop jetzt nur im klassischen Stil (HH:MM). Also 19:15, 20:15, etc.
- Weiterer Datenpunkt "selected_channel_favorite" löst die selben Ereignisse wie "selected_channel" aus. Senderdetails für Visualisierung und in der Hauptanzeige alle Sendungen zum gewählten Kanal.
- Zentrale Sendereditierung. Jetzt können über den Datenpunkt "sender_edit_list" die wichtigsten Einstellungen pro Sender zentral vorgenommen werden. Dazu wird der Inhalt des Datenpunktes (json) angepasst. Dazu kann der Sender den Favoriten (favorite) hinzugefügt, aktiviert oder deaktiviert (active) werden. Darüber hinaus sit es möglich, die Senderreihenfolge (order) festzulegen. Wenn alle Änderungen durchgeführt - einfach speichern und ein Trigger erledigt automatisch den Rest (Daten werden an Sender übergeben und Einstellungen werden sofort aktiv).
-
Hallo. Heute mal einen kleinen Einblick in mein neustes Projekt, mit etwas KI Unterstützung. Knapp 700 Zeilen Code liefern das hier:


Ca. 170 Sender. Favoritensteuerung, Senderreihenfolge, automatische Aktualisierung.

Oder alle anstehenden Sendungen zu einem Sender.EPG-TV für ioBroker
Mit diesem Script lassen sich aktuelle TV-Programmdaten (EPG) von über 160 deutschen TV-Sendern direkt in ioBroker integrieren. Die Daten werden automatisch aus einer XMLTV-Quelle geladen und als Datenpunkte zur Verfügung gestellt.
Neben den eigentlichen Programminformationen bietet das Script eine komfortable Senderverwaltung mit Favoritenfunktion, individueller Sortierung sowie umfangreichen Filtermöglichkeiten. Dadurch eignet es sich hervorragend für Visualisierungen in Jarvis, VIS oder anderen Dashboards.
Funktionen
- Über 160 deutsche TV-Sender
- Free-TV und Pay-TV
- Automatischer Import der EPG-Daten
- Aktuelle und kommende Sendungen
- Senderlogos
- Favoritenverwaltung
- Individuelle Senderreihenfolge
- Filter nach Senderart und Kategorie
- Automatische tägliche Aktualisierung
Voraussetzungen
Für das Script werden folgende zusätzliche NPM-Module benötigt:
Modul Verwendung xml2jsEinlesen und Verarbeiten der XMLTV-Daten user-agentsErzeugung eines Browser-User-Agents für den Abruf der EPG-Daten Die Module können im JavaScript-Adapter unter „Zusätzliche NPM-Module“ eingetragen werden. Anschließend den JavaScript-Adapter neu starten.
Visualisierung
Die Ausgabe erfolgt hauptsächlich über den Datenpunkt
broadcast_list.Für die Darstellung verwende ich das JSON-Widget aus VIS Material Design ab Version 0.5.94. Die Programmdaten werden bereits als fertiges JSON bereitgestellt und können direkt im Widget verarbeitet werden.
Wichtige Hinweise
Beim ersten Start werden automatisch alle verfügbaren Sender aus der XMLTV-Quelle eingelesen und die benötigten Datenpunkte angelegt.
Folgende Datenpunkte werden vollständig vom Script verwaltet und sollten nicht manuell geändert werden:
sender_jsonactive_sender_listfavorite_sender_listbroadcast_list
Die Senderbearbeitung erfolgt ausschließlich über die dafür vorgesehenen Datenpunkte im Bereich
edit.*.Wichtige Datenpunkte
Datenpunkt Bedeutung Verwendung sender_jsonInterne Senderliste Wird vom Script zur Senderverwaltung verwendet active_sender_listAlle aktiven Sender Für Dropdowns und Senderauswahl favorite_sender_listAlle Favoritensender Für Favoritenlisten und Senderauswahl broadcast_listProgrammausgabe Hauptdatenpunkt für Tabellen, Listen und TV-Guides selected_channelAusgewählter Sender Zeigt aktuelle und kommende Sendungen eines Senders refreshManueller Reload Lädt Sender- und EPG-Daten neu filter_*Filterfunktionen Filterung nach Favoriten, Free-/Pay-TV und Kategorien broadcast_modeAnzeigemodus all= alle Sender,selected= ausgewählter SendercountAnzahl gefundener Sender Kontrollwert nach dem Einlesen max_order_numberHöchste vergebene Sendernummer Hilfswert für die Senderreihenfolge Verfügbare Filter
- Favoriten
- Free-TV
- Pay-TV
- Film
- Serie
- Doku
- Kinder
- Musik
- Shopping
- Sport
- Nachrichten
- Vollprogramm
Automatische Aktualisierung
- Täglicher EPG-Import um 03:00 Uhr
- Manueller Reload über
refresh - Laufende Aktualisierung der Programmliste
Verwendung
Alle aktuellen Sendungen anzeigen
broadcast_mode = allZeigt die aktuell laufenden Sendungen aller aktiven Sender in
broadcast_list.Einzelnen Sender anzeigen
selected_channelauf die gewünschte Sender-ID setzen.Anschließend enthält
broadcast_listdie aktuelle sowie alle kommenden Sendungen dieses Senders.Favoriten verwenden
Sender können als Favoriten markiert und über
favorite_sender_listoder den Filterfilter_favoriteverwendet werden.Sender filtern
Die Ausgabe kann über die verschiedenen
filter_*Datenpunkte nach Senderart oder Kategorie eingeschränkt werden.Hinweis
Dieses Script wurde mit Unterstützung von KI entwickelt und anschließend in ioBroker getestet und angepasst. Trotz umfangreicher Tests können Fehler oder Verbesserungspotenziale nicht ausgeschlossen werden. Rückmeldungen, Fehlerberichte und Verbesserungsvorschläge sind daher jederzeit willkommen.Hier das Script:
// === EPG - TV-Daten === //Erst-Version 1.0.0 - 22.06.2026 //Version 1.0.1 - 24.06.2026 //Ersteller Ro75. //Voraussetzungen (Version 1.0.1 getestet mit) //NodeJS: 22.22.x //Javascript-Adapter: 9.0.18 //Admin-Adapter: 7.8.23 //js-controler: 7.2.2 //********** START KONFIGURATION ********** const root = "0_userdata.0.epg."; const data = root + "data."; const sender = root + "sender_json"; //********** AB HIER NICHTS MEHR ÄNDERN ********** const xml2js = require("xml2js"); const UserAgent = require("user-agents"); let select_channel_mode = 0; let loading = false; const CONFIG = { "1": { url_id: "1-2-3tv.de", art: "Free", typ: "Shopping", name: "1-2-3.tv" }, "2": { url_id: "13THSTREET.de", art: "Pay", typ: "Film", name: "13th Street" }, "3": { url_id: "3sat.de", art: "Free", typ: "Vollprogramm", name: "3sat" }, "4": { url_id: "atv.de", art: "Free", typ: "Vollprogramm", name: "ATV" }, "5": { url_id: "AnimalPlanet.de", art: "Pay", typ: "Doku", name: "Animal Planet" }, "6": { url_id: "ANIXESerie.de", art: "Free", typ: "Serie", name: "ANIXE Serie" }, "7": { url_id: "ANIXE+.de", art: "Free", typ: "Vollprogramm", name: "ANIXE+" }, "8": { url_id: "ARD-alpha.de", art: "Free", typ: "Doku", name: "ARD alpha" }, "9": { url_id: "ARTE.de", art: "Free", typ: "Doku", name: "ARTE" }, "10": { url_id: "automotorundsport.de", art: "Pay", typ: "Doku", name: "auto motor und sport" }, "11": { url_id: "AXNBlack.de", art: "Pay", typ: "Film", name: "AXN Black" }, "12": { url_id: "AXNWhite.de", art: "Pay", typ: "Serie", name: "AXN White" }, "13": { url_id: "Beate-UhseTV.de", art: "Pay", typ: "Film", name: "Beate-Uhse.TV" }, "14": { url_id: "BR.de", art: "Free", typ: "Vollprogramm", name: "BR Fernsehen" }, "15": { url_id: "CartoonNetwork.de", art: "Pay", typ: "Kinder", name: "Cartoon Network" }, "16": { url_id: "Cartoonito.de", art: "Pay", typ: "Kinder", name: "Cartoonito" }, "17": { url_id: "ComedyCentral.de", art: "Free", typ: "Serie", name: "Comedy Central" }, "18": { url_id: "Crime+Investigation.de", art: "Pay", typ: "Doku", name: "Crime + Investigation" }, "19": { url_id: "CuriosityChannel.de", art: "Pay", typ: "Doku", name: "Curiosity Channel" }, "20": { url_id: "DasErste.de", art: "Free", typ: "Vollprogramm", name: "Das Erste" }, "21": { url_id: "DAZN.de", art: "Pay", typ: "Sport", name: "DAZN" }, "22": { url_id: "DAZN1.de", art: "Pay", typ: "Sport", name: "DAZN 1" }, "23": { url_id: "DAZN2.de", art: "Pay", typ: "Sport", name: "DAZN 2" }, "24": { url_id: "DeluxeDanceByKontor.de", art: "Free", typ: "Musik", name: "DELUXE DANCE by Kontor" }, "25": { url_id: "DeluxeFlashback.de", art: "Free", typ: "Musik", name: "DELUXE FLASHBACK" }, "26": { url_id: "DeluxeLounge.de", art: "Free", typ: "Musik", name: "DELUXE LOUNGE" }, "27": { url_id: "DeluxeMusic.de", art: "Free", typ: "Musik", name: "DELUXE MUSIC" }, "28": { url_id: "DeluxeRap.de", art: "Free", typ: "Musik", name: "DELUXE RAP" }, "29": { url_id: "DeluxeRock.de", art: "Free", typ: "Musik", name: "DELUXE ROCK" }, "30": { url_id: "DeutschesMusikFernsehen.de",art: "Free", typ: "Musik", name: "Deutsches Musik Fernsehen" }, "31": { url_id: "Discovery.de", art: "Pay", typ: "Doku", name: "Discovery Channel" }, "32": { url_id: "DisneyChannel.de", art: "Free", typ: "Kinder", name: "Disney Channel" }, "33": { url_id: "DMAX.de", art: "Free", typ: "Doku", name: "DMAX" }, "34": { url_id: "dokusat.de", art: "Free", typ: "Doku", name: "dokuSAT" }, "35": { url_id: "EDGESport.de", art: "Pay", typ: "Sport", name: "EDGE Sport" }, "36": { url_id: "emsTV.de", art: "Free", typ: "Musik", name: "ems TV" }, "37": { url_id: "eSPORTS1.de", art: "Pay", typ: "Sport", name: "eSPORTS1" }, "38": { url_id: "Eurosport1.de", art: "Free", typ: "Sport", name: "Eurosport 1" }, "39": { url_id: "Eurosport2.de", art: "Pay", typ: "Sport", name: "Eurosport 2" }, "40": { url_id: "Heimatkanal.de", art: "Pay", typ: "Film", name: "Heimatkanal" }, "41": { url_id: "HGTV.de", art: "Free", typ: "Doku", name: "HGTV" }, "42": { url_id: "HISTORY.de", art: "Pay", typ: "Doku", name: "HISTORY" }, "43": { url_id: "hr.de", art: "Free", typ: "Vollprogramm", name: "hr Fernsehen" }, "44": { url_id: "HSE.de", art: "Free", typ: "Shopping", name: "HSE" }, "45": { url_id: "HSE24.de", art: "Free", typ: "Shopping", name: "HSE24" }, "46": { url_id: "Jukebox.de", art: "Free", typ: "Musik", name: "Jukebox" }, "47": { url_id: "JustCooking.de", art: "Free", typ: "Doku", name: "Just Cooking" }, "48": { url_id: "KabelEins.de", art: "Free", typ: "Vollprogramm", name: "kabel eins" }, "49": { url_id: "KabelEinsCLASSICS.de", art: "Pay", typ: "Film", name: "kabel eins CLASSICS" }, "50": { url_id: "KabelEinsDoku.de", art: "Free", typ: "Doku", name: "kabel eins Doku" }, "51": { url_id: "KiKA.de", art: "Free", typ: "Kinder", name: "KiKA" }, "52": { url_id: "KinoweltTV.de", art: "Pay", typ: "Film", name: "Kinowelt TV" }, "53": { url_id: "MDRSachsen.de", art: "Free", typ: "Vollprogramm", name: "MDR Sachsen" }, "54": { url_id: "MTV.de", art: "Pay", typ: "Musik", name: "MTV" }, "55": { url_id: "MTV00s.de", art: "Pay", typ: "Musik", name: "MTV 00s" }, "56": { url_id: "MTV80s.de", art: "Pay", typ: "Musik", name: "MTV 80s" }, "57": { url_id: "MTV90s.de", art: "Pay", typ: "Musik", name: "MTV 90s" }, "58": { url_id: "MTVHits.de", art: "Pay", typ: "Musik", name: "MTV Hits" }, "59": { url_id: "MTVLive.de", art: "Pay", typ: "Musik", name: "MTV Live" }, "60": { url_id: "n-tv.de", art: "Free", typ: "Doku", name: "n-tv" }, "61": { url_id: "N24Doku.de", art: "Free", typ: "Doku", name: "N24 Doku" }, "62": { url_id: "NationalGeographic.de", art: "Pay", typ: "Doku", name: "National Geographic" }, "63": { url_id: "NationalGeographicWILD.de", art: "Pay", typ: "Doku", name: "Nat Geo Wild" }, "64": { url_id: "NDR.de", art: "Free", typ: "Vollprogramm", name: "NDR Fernsehen" }, "65": { url_id: "Nick.de", art: "Free", typ: "Kinder", name: "Nick" }, "66": { url_id: "NickComedyCentral+1.de", art: "Free", typ: "Kinder", name: "Nick / CC +1" }, "67": { url_id: "NickJr.de", art: "Pay", typ: "Kinder", name: "Nick Jr." }, "68": { url_id: "Nicktoons.de", art: "Pay", typ: "Kinder", name: "Nicktoons" }, "69": { url_id: "NITRO.de", art: "Free", typ: "Vollprogramm", name: "NITRO" }, "70": { url_id: "ONE.de", art: "Free", typ: "Vollprogramm", name: "ONE" }, "71": { url_id: "phoenix.de", art: "Free", typ: "Doku", name: "phoenix" }, "72": { url_id: "ProSieben.de", art: "Free", typ: "Vollprogramm", name: "ProSieben" }, "73": { url_id: "ProSiebenFUN.de", art: "Pay", typ: "Serie", name: "ProSieben FUN" }, "74": { url_id: "ProSiebenMAXX.de", art: "Free", typ: "Vollprogramm", name: "ProSieben MAXX" }, "75": { url_id: "QVC.de", art: "Free", typ: "Shopping", name: "QVC" }, "76": { url_id: "rbbBrandenburg.de", art: "Free", typ: "Vollprogramm", name: "rbb Brandenburg" }, "77": { url_id: "RiC.de", art: "Free", typ: "Kinder", name: "RiC" }, "78": { url_id: "RomanceTV.de", art: "Pay", typ: "Film", name: "Romance TV" }, "79": { url_id: "RTL.de", art: "Free", typ: "Vollprogramm", name: "RTL" }, "80": { url_id: "RTLCrime.de", art: "Pay", typ: "Serie", name: "RTL Crime" }, "81": { url_id: "RTLLiving.de", art: "Pay", typ: "Doku", name: "RTL Living" }, "82": { url_id: "RTLPassion.de", art: "Pay", typ: "Serie", name: "RTL Passion" }, "83": { url_id: "RTLplus.de", art: "Free", typ: "Serie", name: "RTLplus" }, "84": { url_id: "RTLup.de", art: "Free", typ: "Vollprogramm", name: "RTLup" }, "85": { url_id: "RTLZWEI.de", art: "Free", typ: "Vollprogramm", name: "RTLZWEI" }, "86": { url_id: "SAT1.de", art: "Free", typ: "Vollprogramm", name: "SAT.1" }, "87": { url_id: "SAT1emotions.de", art: "Pay", typ: "Serie", name: "SAT.1 emotions" }, "88": { url_id: "SAT1GOLD.de", art: "Free", typ: "Vollprogramm", name: "SAT.1 GOLD" }, "89": { url_id: "SchlagerDeluxe.de", art: "Free", typ: "Musik", name: "Schlager Deluxe" }, "90": { url_id: "SchlagerparadiesTV.de", art: "Free", typ: "Musik", name: "Schlagerparadies TV" }, "91": { url_id: "sixx.de", art: "Free", typ: "Vollprogramm", name: "sixx" }, "92": { url_id: "SkyCinemaAction.de", art: "Pay", typ: "Film", name: "Sky Cinema Action" }, "93": { url_id: "SkyCinemaBlockbuster.de", art: "Pay", typ: "Film", name: "Sky Cinema Blockbuster" }, "94": { url_id: "SkyCinemaClassics.de", art: "Pay", typ: "Film", name: "Sky Cinema Classics" }, "96": { url_id: "SkyCinemaPremiere.de", art: "Pay", typ: "Film", name: "Sky Cinema Premiere" }, "97": { url_id: "SkyCrime.de", art: "Pay", typ: "Serie", name: "Sky Crime" }, "98": { url_id: "SkyDocumentaries.de", art: "Pay", typ: "Doku", name: "Sky Documentaries" }, "99": { url_id: "SkyNature.de", art: "Pay", typ: "Doku", name: "Sky Nature" }, "100": { url_id: "SkyOne.de", art: "Pay", typ: "Vollprogramm", name: "Sky One" }, "101": { url_id: "SkySciFi.de", art: "Pay", typ: "Film", name: "Sky Sci-Fi" }, "102": { url_id: "SkyShowcase.de", art: "Pay", typ: "Vollprogramm", name: "Sky Showcase" }, "103": { url_id: "SkySport1.de", art: "Pay", typ: "Sport", name: "Sky Sport 1" }, "104": { url_id: "SkySport10.de", art: "Pay", typ: "Sport", name: "Sky Sport 10" }, "105": { url_id: "SkySport2.de", art: "Pay", typ: "Sport", name: "Sky Sport 2" }, "106": { url_id: "SkySport3.de", art: "Pay", typ: "Sport", name: "Sky Sport 3" }, "107": { url_id: "SkySport4.de", art: "Pay", typ: "Sport", name: "Sky Sport 4" }, "108": { url_id: "SkySport5.de", art: "Pay", typ: "Sport", name: "Sky Sport 5" }, "109": { url_id: "SkySport6.de", art: "Pay", typ: "Sport", name: "Sky Sport 6" }, "110": { url_id: "SkySport7.de", art: "Pay", typ: "Sport", name: "Sky Sport 7" }, "111": { url_id: "SkySport8.de", art: "Pay", typ: "Sport", name: "Sky Sport 8" }, "112": { url_id: "SkySport9.de", art: "Pay", typ: "Sport", name: "Sky Sport 9" }, "113": { url_id: "SkySportAustria1.de", art: "Pay", typ: "Sport", name: "Sky Sport Austria 1" }, "114": { url_id: "SkySportBundesliga.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga" }, "115": { url_id: "SkySportBundesliga1.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 1" }, "116": { url_id: "SkySportBundesliga10.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 10" }, "117": { url_id: "SkySportBundesliga2.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 2" }, "118": { url_id: "SkySportBundesliga3.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 3" }, "119": { url_id: "SkySportBundesliga4.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 4" }, "120": { url_id: "SkySportBundesliga5.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 5" }, "121": { url_id: "SkySportBundesliga6.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 6" }, "122": { url_id: "SkySportBundesliga7.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 7" }, "123": { url_id: "SkySportBundesliga8.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 8" }, "124": { url_id: "SkySportBundesliga9.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga 9" }, "125": { url_id: "SkySportBundesligaUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport Bundesliga UHD" }, "126": { url_id: "SkySportF1.de", art: "Pay", typ: "Sport", name: "Sky Sport F1" }, "127": { url_id: "SkySportGolf.de", art: "Pay", typ: "Sport", name: "Sky Sport Golf" }, "128": { url_id: "SkySportKompakt1.de", art: "Pay", typ: "Sport", name: "Sky Sport Kompakt 1" }, "129": { url_id: "SkySportMix.de", art: "Pay", typ: "Sport", name: "Sky Sport Mix" }, "130": { url_id: "SkySportNews.de", art: "Pay", typ: "Sport", name: "Sky Sport News" }, "131": { url_id: "SkySportPremierLeague.de", art: "Pay", typ: "Sport", name: "Sky Sport Premier League" }, "132": { url_id: "SkySportTennis.de", art: "Pay", typ: "Sport", name: "Sky Sport Tennis" }, "133": { url_id: "SkySportTopEvent.de", art: "Pay", typ: "Sport", name: "Sky Sport Top Event" }, "134": { url_id: "SkySportUHD.de", art: "Pay", typ: "Sport", name: "Sky Sport UHD" }, "135": { url_id: "SkyAtlantic.de", art: "Pay", typ: "Serie", name: "Sky Atlantic" }, "136": { url_id: "SkyKrimi.de", art: "Pay", typ: "Serie", name: "Sky Krimi" }, "137": { url_id: "sonnenklarTV.de", art: "Free", typ: "Shopping", name: "sonnenklar.TV" }, "138": { url_id: "SpiegelGeschichte.de", art: "Pay", typ: "Doku", name: "Spiegel Geschichte" }, "139": { url_id: "SPORT1.de", art: "Free", typ: "Sport", name: "SPORT1" }, "140": { url_id: "SPORT1+.de", art: "Pay", typ: "Sport", name: "SPORT1+" }, "141": { url_id: "SRFernsehen.de", art: "Free", typ: "Vollprogramm", name: "SR Fernsehen" }, "142": { url_id: "SuperRTL.de", art: "Free", typ: "Kinder", name: "SUPER RTL" }, "143": { url_id: "SWRBW.de", art: "Free", typ: "Vollprogramm", name: "SWR BW" }, "144": { url_id: "tagesschau24.de", art: "Free", typ: "Doku", name: "tagesschau24" }, "145": { url_id: "Tele5.de", art: "Free", typ: "Film", name: "Tele 5" }, "146": { url_id: "TLC.de", art: "Free", typ: "Doku", name: "TLC" }, "147": { url_id: "TOGGOplus.de", art: "Free", typ: "Kinder", name: "TOGGO plus" }, "148": { url_id: "UniversalTV.de", art: "Pay", typ: "Serie", name: "Universal TV" }, "149": { url_id: "VolksmusikTV.de", art: "Free", typ: "Musik", name: "Volksmusik TV" }, "150": { url_id: "VOX.de", art: "Free", typ: "Vollprogramm", name: "VOX" }, "151": { url_id: "VOXup.de", art: "Free", typ: "Vollprogramm", name: "VOXup" }, "152": { url_id: "WaidwerkTV.de", art: "Pay", typ: "Doku", name: "Waidwerk TV" }, "153": { url_id: "WarnerTVComedy.de", art: "Pay", typ: "Serie", name: "Warner TV Comedy" }, "154": { url_id: "WarnerTVFilm.de", art: "Pay", typ: "Film", name: "Warner TV Film" }, "155": { url_id: "WarnerTVSerie.de", art: "Pay", typ: "Serie", name: "Warner TV Serie" }, "156": { url_id: "WDR.de", art: "Free", typ: "Vollprogramm", name: "WDR Fernsehen" }, "157": { url_id: "WELT.de", art: "Free", typ: "Doku", name: "WELT" }, "158": { url_id: "WeltderWunder.de", art: "Free", typ: "Doku", name: "Welt der Wunder" }, "159": { url_id: "ZDF.de", art: "Free", typ: "Vollprogramm", name: "ZDF" }, "160": { url_id: "zdf_neo.de", art: "Free", typ: "Vollprogramm", name: "ZDFneo" }, "161": { url_id: "ZDFinfo.de", art: "Free", typ: "Doku", name: "ZDFinfo" } }; async function smartCreateState(id, value, options = {}) { if (existsState(id)) return; await createState(id, value, options); } function sleepMs(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const MAP_URL_TO_ID = {}; for (const key in CONFIG) MAP_URL_TO_ID[CONFIG[key].url_id] = Number(key); function parseXmltvTime(t) { const [stamp, offset] = t.split(" "); const d = new Date(`${stamp.slice(0,4)}-${stamp.slice(4,6)}-${stamp.slice(6,8)}T${stamp.slice(8,10)}:${stamp.slice(10,12)}:${stamp.slice(12,14)}${offset}`); return `${String(d.getDate()).padStart(2,"0")}.${String(d.getMonth()+1).padStart(2,"0")}.${d.getFullYear()} ` + `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}`; } function parseGermanDateTime(str) { const [datePart, timePart] = str.split(" "); const [day, month, year] = datePart.split(".").map(Number); const [hour, minute, second] = timePart.split(":").map(Number); return new Date(year, month - 1, day, hour, minute, second).getTime(); } function setStateIfChanged(id, value, STOPPED = false) { if (STOPPED) return; const state = getState(id); if (!state) return; const old = state.val; const isObject = v => v !== null && typeof v === "object"; if (isObject(value)) { const newStr = JSON.stringify(value); let oldStr = null; if (typeof old === "string") oldStr = old; else if (isObject(old)) oldStr = JSON.stringify(old); if (newStr === oldStr) return; return setState(id, newStr, true); } if (old === value) return; setState(id, value, true); } async function main() { loading = true; await smartCreateState(sender, "[]", {name: "Senderliste", type: "string", read: true, write: false}); await smartCreateState(root + "count", 0, {name: "Anzahl", type: "number", read: true, write: false}); await smartCreateState(root + "refresh", false, {name: "Aktualisieren", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_favorite", false, {name: "Filter nur Favoriten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_free", false, {name: "Filter nur Free-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_pay", false, {name: "Filter nur Pay-TV", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_film", false, {name: "Filter Typ Film", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_serie", false, {name: "Filter Typ Serie", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_doku", false, {name: "Filter Typ Doku", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_kinder", false, {name: "Filter Typ Kinder", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_musik", false, {name: "Filter Typ Musik", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_shopping", false, {name: "Filter Typ Shopping", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_sport", false, {name: "Filter Typ Sport", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_nachrichten", false, {name: "Filter Typ Nachrichten", type: "boolean", read: true, write: true}); await smartCreateState(root + "filter_typ_vollprogramm", false, {name: "Filter Typ Vollprogramm", type: "boolean", read: true, write: true}); await smartCreateState(root + "broadcast_mode", "all", {name: "Broadcast Modus", type: "string", read: true, write: true}); await smartCreateState(root + "broadcast_list", "[]", {name: "aktuelle Sendungen", type: "string", read: true, write: false}); await smartCreateState(root + "selected_channel", 0, {name: "Ausgewählter Sender", type: "number", read: true, write: true}); await smartCreateState(root + "selected_channel_favorite", 0, {name: "Ausgewählter Favorit-Sender", type: "number", read: true, write: true}); await smartCreateState(root + "active_sender_list", "[]", {name: "alle aktiven Sender mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "favorite_sender_list", "[]", {name: "alle Favoriten mit ID", type: "string", read: true, write: false}); await smartCreateState(root + "max_order_number", 0, {name: "Höchste vergebene Sendernummer", type: "number", read: true, write: false}); await smartCreateState(root + "sender_edit_list", "[]", {name: "Editierungsliste", type: "string", read: true, write: true}); await smartCreateState(root + "edit.active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(root + "edit.icon_url", "", { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(root + "edit.art", "", { name: "Art", type: "string", read: true, write: false }); await smartCreateState(root + "edit.typ", "", { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(root + "edit.name", "", { name: "Name", type: "string", read: true, write: true }); await smartCreateState(root + "edit.order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); await smartCreateState(root + "edit.save", false, { name: "Speichern", type: "boolean", read: true, write: true }); await sleepMs(200); await loadChannelsAndProgrammes(); await sleepMs(500); await createSenderList("active"); await createSenderList("favorite"); await sleepMs(200); await initSenderEditList(); await sleepMs(2000); await updateAllActiveBroadcasts(); registerTriggers(); loading = false; } main(); function senderPassesFilters(internal_id) { const base = data + internal_id; const active = getState(base + ".active")?.val; if (!active) return false; const favoriteOnly = getState(root + "filter_favorite")?.val === true; if (favoriteOnly) { const fav = getState(base + ".favorite")?.val; if (!fav) return false; } const allowedArt = []; if (getState(root + "filter_free")?.val === true) allowedArt.push("Free"); if (getState(root + "filter_pay")?.val === true) allowedArt.push("Pay"); const art = getState(base + ".art")?.val; if (allowedArt.length > 0 && !allowedArt.includes(art)) return false; const senderTyp = getState(base + ".typ")?.val; const allowedTypes = []; if (getState(root + "filter_typ_film")?.val === true) allowedTypes.push("Film"); if (getState(root + "filter_typ_serie")?.val === true) allowedTypes.push("Serie"); if (getState(root + "filter_typ_doku")?.val === true) allowedTypes.push("Doku"); if (getState(root + "filter_typ_kinder")?.val === true) allowedTypes.push("Kinder"); if (getState(root + "filter_typ_musik")?.val === true) allowedTypes.push("Musik"); if (getState(root + "filter_typ_shopping")?.val === true) allowedTypes.push("Shopping"); if (getState(root + "filter_typ_sport")?.val === true) allowedTypes.push("Sport"); if (getState(root + "filter_typ_nachrichten")?.val === true) allowedTypes.push("Nachrichten"); if (getState(root + "filter_typ_vollprogramm")?.val === true) allowedTypes.push("Vollprogramm"); if (allowedTypes.length > 0 && !allowedTypes.includes(senderTyp)) return false; return true; } async function updateMaxOrderNumber() { const senderList = JSON.parse(getState(sender).val || "[]"); let max = 0; for (const s of senderList) { const order = getState(data + s.internal_id + ".order")?.val || 0; if (order > max) max = order; } setStateIfChanged(root + "max_order_number", max); return max; } async function loadSenderIntoEdit(id) { const base = data + id; setStateIfChanged(root + "edit.active", getState(base + ".active")?.val || false); setStateIfChanged(root + "edit.favorite", getState(base + ".favorite")?.val || false); setStateIfChanged(root + "edit.icon_url", getState(base + ".icon_url")?.val || ""); setStateIfChanged(root + "edit.art", getState(base + ".art")?.val || ""); setStateIfChanged(root + "edit.typ", getState(base + ".typ")?.val || ""); setStateIfChanged(root + "edit.name", getState(base + ".name")?.val || ""); setStateIfChanged(root + "edit.order", getState(base + ".order")?.val || 0); } async function saveEditToSender(id) { const base = data + id; setStateIfChanged(base + ".active", getState(root + "edit.active")?.val); setStateIfChanged(base + ".favorite", getState(root + "edit.favorite")?.val); setStateIfChanged(base + ".order", getState(root + "edit.order")?.val); } async function createEditSenderList() { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; const name = getState(base + ".name")?.val || ""; const order = Number(getState(base + ".order")?.val || 0); const favorite = getState(base + ".favorite")?.val === true; const active = getState(base + ".active")?.val === true; result.push({ text: name, value: s.internal_id, order: order, favorite: favorite, active: active }); } result.sort((a, b) => { const oa = a.order; const ob = b.order; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.text.localeCompare(b.text); }); return result; } async function initSenderEditList() { const list = await createEditSenderList(); setStateIfChanged(root + "sender_edit_list", JSON.stringify(list)); } async function loadChannelsAndProgrammes() { try { const ua = new UserAgent().toString(); const response = await httpGetAsync("https://iptv-epg.org/files/epg-de.xml", { timeout: 20000, headers: { "User-Agent": ua } }); if (!response || response.statusCode !== 200) return; const xml = response.data; const result = await xml2js.parseStringPromise(xml); const channels = result.tv.channel .filter(ch => MAP_URL_TO_ID[ch.$.id]) .map(ch => ({ id: ch.$.id, name: Array.isArray(ch["display-name"]) ? (typeof ch["display-name"][0] === "string" ? ch["display-name"][0] : ch["display-name"][0]._ || "") : "", internal_id: MAP_URL_TO_ID[ch.$.id], icon_url: ch.icon?.[0]?.$.src || "" })) .sort((a, b) => a.internal_id - b.internal_id); setStateIfChanged(root + "count", channels.length); setStateIfChanged(sender, JSON.stringify(channels.map(c => ({id: c.id, name: CONFIG[c.internal_id].name, internal_id: c.internal_id})))); const programmes = result.tv.programme .filter(p => MAP_URL_TO_ID[p.$.channel]) .map(p => ({ internal_id: MAP_URL_TO_ID[p.$.channel], start: parseXmltvTime(p.$.start), stop: parseXmltvTime(p.$.stop), title: Array.isArray(p.title) ? (p.title[0]._ || p.title[0]) : "", desc: Array.isArray(p.desc) ? (p.desc[0]._ || p.desc[0]) : "", date: Array.isArray(p.date) ? p.date[0] : "", episode: Array.isArray(p["episode-num"]) ? p["episode-num"][0] : "", categories: Array.isArray(p.category) ? p.category.map(c => (typeof c === "string" ? c : c._ || "")) : [], icon: p.icon?.[0]?.$.src || "" })); const grouped = {}; for (const p of programmes) { if (!grouped[p.internal_id]) grouped[p.internal_id] = []; grouped[p.internal_id].push(p); } for (const ch of channels) { const list = grouped[ch.internal_id] || []; const base = data + ch.internal_id; const now = Date.now(); const filtered = list.filter(p => { const stopTs = new Date( p.stop.slice(6,10) + "-" + p.stop.slice(3,5) + "-" + p.stop.slice(0,2) + "T" + p.stop.slice(11,19) ).getTime(); return stopTs >= now; }); // internal_id aus JSON entfernen const cleaned = filtered.map(p => { const { internal_id, ...rest } = p; return rest; }); await smartCreateState(base + ".broadcast", "[]", { name: "Sendungen", type: "string", read: true, write: false }); await smartCreateState(base + ".active", true, { name: "Aktiv", type: "boolean", read: true, write: true }); await smartCreateState(base + ".favorite", false, { name: "Favorit", type: "boolean", read: true, write: true }); await smartCreateState(base + ".icon_url", ch.icon_url, { name: "Icon URL", type: "string", read: true, write: true }); await smartCreateState(base + ".art", CONFIG[ch.internal_id].art, { name: "Art", type: "string", read: true, write: false }); await smartCreateState(base + ".typ", CONFIG[ch.internal_id].typ, { name: "Typ", type: "string", read: true, write: false }); await smartCreateState(base + ".name", CONFIG[ch.internal_id].name, { name: "Name", type: "string", read: true, write: true }); await smartCreateState(base + ".order", 0, {name: "Sender Reihenfolge", type: "number", read: true, write: true}); setStateIfChanged(base + ".broadcast", JSON.stringify(cleaned)); } } catch (err) { log(err, "error"); } } async function createSenderList(mode) { const result = []; const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { const base = data + s.internal_id; if (mode === "favorite") { const favorite = getState(base + ".favorite")?.val; if (!favorite) continue; } const senderName = getState(base + ".name")?.val || ""; result.push({text: senderName, value: s.internal_id }); } if (mode === "favorite") { result.sort((a, b) => { const orderA = getState(data + a.value + ".order")?.val || 0; const orderB = getState(data + b.value + ".order")?.val || 0; if (orderA > 0 && orderB === 0) return -1; if (orderA === 0 && orderB > 0) return 1; if (orderA > 0 && orderB > 0) return orderA - orderB; return a.text.localeCompare(b.text); }); } else { result.sort((a, b) => a.text.localeCompare(b.text)); } if (mode === "active") { setStateIfChanged(root + "active_sender_list", JSON.stringify(result)); } else if (mode === "favorite") { setStateIfChanged(root + "favorite_sender_list", JSON.stringify(result)); } return result; } async function updateAllActiveBroadcasts() { const result = []; const now = Date.now(); const senderList = JSON.parse(getState(sender).val || "[]"); for (const s of senderList) { if (!senderPassesFilters(s.internal_id)) continue; const base = data + s.internal_id; const order = getState(base + ".order")?.val || 0; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für Sender " + s.internal_id + ": " + e, "warn"); continue; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const start_ts = parseGermanDateTime(entry.start); const stop_ts = parseGermanDateTime(entry.stop); if (now >= start_ts && now < stop_ts) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon, order: order }); break; } } } result.sort((a, b) => { const oa = a.order || 0; const ob = b.order || 0; if (oa > 0 && ob === 0) return -1; if (oa === 0 && ob > 0) return 1; if (oa > 0 && ob > 0) return oa - ob; return a.sender_name.localeCompare(b.sender_name); }); setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } async function updateSelectedChannelBroadcast() { const result = []; const now = Date.now(); let selected; if (select_channel_mode == 1) { selected = getState(root + "selected_channel")?.val; } else { selected = getState(root + "selected_channel_favorite")?.val; } if (!selected) return []; const base = data + selected; const raw = getState(base + ".broadcast")?.val || "[]"; let list = []; try { list = JSON.parse(raw); } catch (e) { log("Fehler beim JSON-Parse für selected_channel " + selected + ": " + e, "warn"); return []; } const senderIcon = getState(base + ".icon_url")?.val || ""; const senderName = getState(base + ".name")?.val || ""; for (const entry of list) { const stop_ts = parseGermanDateTime(entry.stop); if (stop_ts > now) { const startTime = entry.start.split(" ")[1].slice(0,5); const stopTime = entry.stop.split(" ")[1].slice(0,5); result.push({ sender_icon_url: `<img style="max-width:80px; max-height:80px; object-fit:contain; vertical-align:middle;" src="${senderIcon}">`, sender_name: senderName, start: startTime, stop: stopTime, title: entry.title, desc: entry.desc, date: entry.date, icon: entry.icon}); } } setStateIfChanged(root + "broadcast_list", JSON.stringify(result)); return result; } schedule("0 3 * * *", async () => { await loadChannelsAndProgrammes(); await initSenderEditList(); }); schedule("1 * * * * *", async () => { if (loading === true) return; const mode = getState(root + "broadcast_mode")?.val; if (mode === "selected") { await updateSelectedChannelBroadcast(); } else { await updateAllActiveBroadcasts(); } }); function registerTriggers() { on({ id: root + "refresh", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { await loadChannelsAndProgrammes(); await createSenderList("active"); await createSenderList("favorite"); await initSenderEditList(); await updateMaxOrderNumber(); } catch (err) {} setState(root + "refresh", false, true); } }); on({ id: root + "edit.save", change: "ne" }, async (obj) => { if (obj.state && obj.state.ack === false && obj.state.val === true) { try { if (select_channel_mode == 1) { await saveEditToSender(getState(root + "selected_channel").val); } else if (select_channel_mode == 2) { await saveEditToSender(getState(root + "selected_channel_favorite").val); } await createSenderList("active"); await createSenderList("favorite"); } catch (err) {} setState(root + "edit.save", false, true); } }); on({ id: root + "selected_channel", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 1; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); on({ id: root + "selected_channel_favorite", change: "ne" }, async obj => { const val = obj.state.val; if (!val) { setStateIfChanged(root + "broadcast_mode", "all"); } else { select_channel_mode = 2; setStateIfChanged(root + "broadcast_mode", "selected"); await updateSelectedChannelBroadcast(); await updateMaxOrderNumber(); await loadSenderIntoEdit(val); } }); const TYPE_FILTERS = [root + "filter_favorite", root + "filter_free", root + "filter_pay", root + "filter_typ_film", root + "filter_typ_serie", root + "filter_typ_doku", root + "filter_typ_kinder", root + "filter_typ_musik", root + "filter_typ_shopping", root + "filter_typ_sport", root + "filter_typ_nachrichten", root + "filter_typ_vollprogramm"]; on({ id: TYPE_FILTERS, change: "ne" }, async () => { await updateAllActiveBroadcasts(); }); on({ id: root + "broadcast_mode", change: "ne" }, async obj => { const val = obj.state.val; if (val === 'all') { await updateAllActiveBroadcasts(); } }); on({ id: root + "sender_edit_list", change: "ne" }, async obj => { if (!obj || !obj.state || !obj.state.val) return; let list; try { list = JSON.parse(obj.state.val); } catch (e) { log("Fehler: sender_edit_list enthält kein gültiges JSON", "warn"); return; } for (const entry of list) { const id = entry.value; const base = data + id; setStateIfChanged(base + ".order", Number(entry.order || 0)); setStateIfChanged(base + ".favorite", entry.favorite === true); setStateIfChanged(base + ".active", entry.active === true); } await initSenderEditList(); }); }Viel Spaß und Erfolg bei der Umsetzung.
Ro75.
Änderung 1.0.1 - 24.06.2026
- Start und Stop jetzt nur im klassischen Stil (HH:MM). Also 19:15, 20:15, etc.
- Weiterer Datenpunkt "selected_channel_favorite" löst die selben Ereignisse wie "selected_channel" aus. Senderdetails für Visualisierung und in der Hauptanzeige alle Sendungen zum gewählten Kanal.
- Zentrale Sendereditierung. Jetzt können über den Datenpunkt "sender_edit_list" die wichtigsten Einstellungen pro Sender zentral vorgenommen werden. Dazu wird der Inhalt des Datenpunktes (json) angepasst. Dazu kann der Sender den Favoriten (favorite) hinzugefügt, aktiviert oder deaktiviert (active) werden. Darüber hinaus sit es möglich, die Senderreihenfolge (order) festzulegen. Wenn alle Änderungen durchgeführt - einfach speichern und ein Trigger erledigt automatisch den Rest (Daten werden an Sender übergeben und Einstellungen werden sofort aktiv).
-
-
Ich werde für die zwei Auswahlfelder getrennte Datenpunkte machen.

Am Ende lösen zwar beide die selbe Aktion aus, ABER in der Darstellung bleiben beide befüllt und die Favoritenauswahl

kann nicht mehr leer erscheinen. Also so wie im unteren Bild zu sehen - das wird dann nicht mehr passieren.
Ro75.
-
Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.
Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.
Mit deinem Input könnte dieser Beitrag noch besser werden 💗
Registrieren Anmelden

