Einrichtung eines Skripts, das den Google‑Kontakte‑Geburtstage‑Kalender in einen eigenen Zielkalender kopiert (z. B. für Freigabe auf Mobilgeräten). Enthalten sind: Voraussetzungen, Identifikation der Kalender‑IDs, Berechtigungen, Skript, Trigger, Tests, Logging sowie Troubleshooting. Ziel: Täglich (oder stündlich) alle Geburtstage im Fenster ±12 Monate synchronisieren. Vorhandene Einträge im Zielzeitraum werden vorher gelöscht, sodass du keine Duplikate bekommst.
Find a file
2025-09-03 04:38:06 +02:00
src Kalender ID entfernt 2025-09-03 04:19:08 +02:00
.gitignore Initial commit 2025-09-02 01:58:57 +00:00
LICENSE Initial commit 2025-09-02 01:58:57 +00:00
README.md Version 2025-09-03 04:38:06 +02:00

Google Kontakte → Kalender: Geburtstage-Sync (People API)

Language Version ioBroker

Diese Anleitung ist EndtoEnd und für Einsteiger geeignet. Sie umfasst: Hintergrund (warum der alte GeburtstagsKalender nicht mehr geht), Einrichtung der People API (Advanced Service + Cloud Console), Script Properties für eine flexible KalenderID, das fertige Skript (mit Alter im Titel), Trigger, Fehlerdiagnose und FAQ. Alle Schritte sind klickbar verlinkt.


Warum nicht mehr addressbook#contacts…?

Der frühere GeburtstagsKalender war ein echter Kalender mit fester ID. Heute ist „Geburtstage“ oft nur noch eine OverlayAnsicht ohne ID → in Apps Script nicht mehr direkt nutzbar. Lösung: Geburtstage direkt aus Google Kontakte (People API) lesen und in einen echten Zielkalender schreiben.


Voraussetzungen

  • Google Konto mit Google Kontakte und Google Kalender.
  • Zugriff auf Apps Script: <https://script.google.com>
  • (Empfohlen) Eigener Zielkalender, z.B. „Geburtstage (Sync)“.

Schritt 1 Zielkalender anlegen & ID kopieren

  1. Öffne Google KalenderZahnradEinstellungen+ Kalender hinzufügenNeuen Kalender erstellen (z.B. „Geburtstage (Sync)“).
  2. In den Kalendereinstellungen die KalenderID kopieren (endet auf @group.calendar.google.com).

Diese ID speichern wir gleich als Script Property, damit der Code nicht hardcodiert ist.


Schritt 2 Apps Script Projekt & Zeitzone

  1. Öffne <https://script.google.com> → Neues Projekt → Name z.B. Birthdays Sync.
  2. ProjektEinstellungen (Zahnradsymbol) → Zeitzone auf Europe/Berlin stellen.

Schritt 3 People API aktivieren (2 Schalter)

  1. Advanced Google services in Apps Script aktivieren:
    Link: <https://developers.google.com/apps-script/guides/services/advanced>
    Im Editor links auf Services (Puzzleteil) → Add a servicePeople APIAdd.
  2. People API im verknüpften Google CloudProjekt aktivieren:
    Link: <https://developers.google.com/people/v1/getting-started> → „use the setup tool“
    Direkt zur Console: <https://console.cloud.google.com/apis/library/people.googleapis.com> → Enable.
    (Allgemeine Anleitung: <https://support.google.com/googleapi/answer/6158841>)

Ohne beide Schritte ist People.* in Apps Script nicht verfügbar.


Schritt 4 Script Property für die KalenderID setzen

Script Properties sind Key/ValuePaare fürs Projekt.

  1. Apps Script → Datei → Projekteigenschaften → Script Properties.
  2. Neu: Key BIRTHDAY_CALENDAR_ID → Value = deine KalenderID (z.B. …@group.calendar.google.com) → Speichern.

Dokumentation:


Schritt 5 Fertiger Code (mit Alter im Titel + PropertiesSupport)

Hinweis: Der Code löscht nur eigene Events (MARKER) im Fenster ±360 Tage und schreibt jeden Geburtstag für dieses & nächstes Jahr. Bei vorhandenem Geburtsjahr wird das Alter in Klammern angezeigt.

/**
 * Synchronisiert Geburtstage aus Google Kontakte (People API) in einen Zielkalender.
 * — Löscht zuvor nur eigene Script-Events (MARKER) im Fenster ±360 Tage.
 * — Schreibt für aktuelles & nächstes Jahr; bei bekanntem Geburtsjahr mit Alter im Titel.
 * — Zeitzone: Europe/Berlin (in Projekt-Einstellungen setzen!).
 *
 * Einrichtung:
 * 1) Advanced Google service „People API“ aktivieren (Editor → Services → Add → People API).
 * 2) People API im Google-Cloud-Projekt aktivieren (Console → APIs & Services → Library → People API → Enable).
 * 3) Script Property setzen: BIRTHDAY_CALENDAR_ID = <deine @group.calendar.google.com-ID>.
 */
function runSync() {
  var props = PropertiesService.getScriptProperties();
  var calendarId = props.getProperty("BIRTHDAY_CALENDAR_ID");
  if (!calendarId) throw new Error("Script Property BIRTHDAY_CALENDAR_ID ist nicht gesetzt.");
  Sync_Birthdays_PeopleOnly_WithAge(calendarId, "SyncedBy=BirthdaysScript");
}

/**
 * Kernfunktion: Liest Kontakte (People API) und erzeugt All-Day-Events im Zielkalender.
 * @param {string} calendarId Zielkalender-ID (…@group.calendar.google.com)
 * @param {string} [marker]   Markertext zur Kennzeichnung Script-erzeugter Events
 */
function Sync_Birthdays_PeopleOnly_WithAge(calendarId, marker) {
  marker = marker || "SyncedBy=BirthdaysScript";

  var tz = Session.getScriptTimeZone() || "Europe/Berlin";
  var dst = CalendarApp.getCalendarById(calendarId);
  if (!dst) throw new Error("Zielkalender nicht gefunden: " + calendarId);

  var today = new Date();
  var startWin = atStartOfDay(addDays(today, -360), tz);
  var endWin   = atEndOfDay(addDays(today,  360), tz);

  // 1) Eigene alten Einträge im Fenster entfernen
  dst.getEvents(startWin, endWin).forEach(function(ev){
    var d = ev.getDescription() || "";
    if (d.indexOf(marker) !== -1) { try { ev.deleteEvent(); } catch(_){} }
  });

  // 2) Kontakte lesen & Geburtstage schreiben
  var created = 0, pageToken = null;
  do {
    var resp = People.People.Connections.list("people/me", {
      personFields: "names,birthdays,events",
      sources: ["READ_SOURCE_TYPE_CONTACT"],
      pageSize: 1000,
      pageToken: pageToken
    });

    var conns = (resp && resp.connections) ? resp.connections : [];
    conns.forEach(function(p){
      var name = (p.names && p.names.length && p.names[0].displayName) ? p.names[0].displayName : null;
      if (!name) return;

      // Geburtstag aus birthdays oder events holen
      var bd = pickBirthdayFromBirthdays(p.birthdays) || pickBirthdayFromEvents(p.events);
      if (!bd || !bd.month || !bd.day) return; // nur echte Einträge (Monat+Tag)

      // Immer aktuelles & nächstes Jahr
      [today.getFullYear(), today.getFullYear() + 1].forEach(function(yr){
        var day = (bd.month === 2 && bd.day === 29 && !isLeap(yr)) ? 28 : bd.day;
        var when = new Date(yr, bd.month - 1, day);
        if (when < startWin || when > endWin) return;

        var title = "🎂 " + name;
        if (bd.year) {
          var age = yr - bd.year;
          if (age >= 0 && age <= 150) title += " (" + age + ")";
        }

        try {
          dst.createAllDayEvent(title, when, { description: marker });
          created++;
        } catch(_) {}
      });
    });

    pageToken = resp && resp.nextPageToken ? resp.nextPageToken : null;
  } while (pageToken);

  console.log("Geburtstage erstellt:", created);
}

/**
 * Wählt den primären Geburtstag aus dem Array `birthdays`; Fallback erster gültiger.
 * @param {Array} arr People API birthdays-Array
 * @returns {{year:(number|null),month:number,day:number}|null}
 */
function pickBirthdayFromBirthdays(arr) {
  if (!arr || !arr.length) return null;
  var primary = arr.find(function(b){ return b && b.metadata && b.metadata.primary && b.date; });
  var any     = primary || arr.find(function(b){ return b && b.date; });
  if (!any || !any.date) return null;
  return { year: any.date.year || null, month: any.date.month, day: any.date.day };
}

/**
 * Extrahiert Geburtstag aus People API events (type=BIRTHDAY).
 * @param {Array} arr People API events-Array
 * @returns {{year:(number|null),month:number,day:number}|null}
 */
function pickBirthdayFromEvents(arr) {
  if (!arr || !arr.length) return null;
  var ev = arr.find(function(e){ return e && e.type && String(e.type).toUpperCase() === "BIRTHDAY" && e.date; });
  if (!ev || !ev.date) return null;
  return { year: ev.date.year || null, month: ev.date.month, day: ev.date.day };
}

/* === Hilfsfunktionen === */

/** Addiert Tage auf ein Datum. */
function addDays(date, days){ var d=new Date(date); d.setDate(d.getDate()+days); return d; }

/** Liefert Mitternacht (00:00:00) im angegebenen Zeitzonen-Format. */
function atStartOfDay(d, tz){ return new Date(Utilities.formatDate(d, tz, "yyyy-MM-dd'T'00:00:00")); }

/** Liefert 23:59:59.999 im angegebenen Zeitzonen-Format. */
function atEndOfDay(d, tz){ var dd=new Date(Utilities.formatDate(d, tz, "yyyy-MM-dd'T'23:59:59")); dd.setMilliseconds(999); return dd; }

/** Prüft, ob ein Jahr ein Schaltjahr ist. */
function isLeap(y){ return (y%4===0 && y%100!==0) || (y%400===0); }

Schritt 6 Erster manueller Lauf (Scopes erteilen)

  1. Im Editor oben Funktion auswählen: runSyncAusführen.
  2. Ersten Berechtigungsdialog bestätigen (People & Kalender).
  3. Danach im Zielkalender (F5) prüfen: neue 🎂Einträge sollten sichtbar sein.

Dokumentation:


Schritt 7 Zeitgesteuerten Trigger einrichten

  1. Links Auslöser (WeckerIcon) → Auslöser hinzufügen.
  2. Funktion: runSync
  3. Ereignisquelle: Zeitgesteuert → z.B. Täglich oder StündlichSpeichern.

TriggerGuide: <https://developers.google.com/apps-script/guides/triggers>


Troubleshooting

  • **TypeError: People is undefined** → Advanced Service People API in Apps Script nicht aktiviert. (Schritt 3.1)
  • **GoogleJsonResponseException: Access Not Configured** → People API in der Cloud Console nicht aktiviert. (Schritt 3.2)
  • 0 Einträge erstellt → In deinen Kontakten sind ggf. keine echten Geburtstage (Monat+Tag) gespeichert. Trage beim Kontakt im Feld „Geburtstag“ ein Datum ein.
  • Keine Schreibrechte → Prüfe die KalenderID und dass dein Konto Schreibrechte auf dem Zielkalender hat.
  • 29. Februar → wird automatisch auf 28. Februar gesetzt, wenn kein Schaltjahr.

FAQ

Zeigt ihr Alter an, wenn kein Geburtsjahr vorhanden ist?
Nein. Ohne Geburtsjahr wird nur 🎂 Name eingetragen.

Kann ich statt Script Property einen Parameter nutzen?
Ja: Sync_Birthdays_PeopleOnly_WithAge("<kalender-id>"). Für Deployments über mehrere Umgebungen sind Script Properties komfortabler.

Wird der Zielkalender geleert?
Nur Events mit MARKER im ±360TageFenster werden entfernt andere Einträge bleiben unangetastet.

Kann ich das Zeitfenster ändern?
Ja, im Code addDays(today, -360) / +360 anpassen (z.B. ±720 Tage).



### Fertig. 

Du kannst jetzt ohne CodeAnpassung den Zielkalender per Script Property wechseln, regelmäßig per Trigger synchronisieren und bei vorhandenem Geburtsjahr wird automatisch das Alter angezeigt.

Beispiel: mehrere Kalender

/**
 * Sync-Geburtstage aus Google Kontakte (People API) in mehrere Zielkalender.
 * 
 * Script Property BIRTHDAY_CALENDAR_IDS = Komma-getrennte IDs:
 *   z.B.: "kal1@group.calendar.google.com,kal2@group.calendar.google.com"
 */
function runSyncMultiple() {
  var props = PropertiesService.getScriptProperties();
  var idsString = props.getProperty("BIRTHDAY_CALENDAR_IDS");
  if (!idsString) throw new Error("Script Property BIRTHDAY_CALENDAR_IDS ist nicht gesetzt!");

  var calendarIds = idsString.split(",").map(function(id){ return id.trim(); }).filter(Boolean);
  if (!calendarIds.length) throw new Error("Keine gültigen Kalender-IDs gefunden.");

  Sync_Birthdays_MultipleCalendars(calendarIds, "SyncedBy=BirthdaysScript");
}

/**
 * Kernfunktion: liest Kontakte und schreibt Geburtstage in mehrere Kalender.
 * @param {string[]} calendarIds Array von Kalender-IDs
 * @param {string} marker Markertext für eigene Events
 */
function Sync_Birthdays_MultipleCalendars(calendarIds, marker) {
  marker = marker || "SyncedBy=BirthdaysScript";

  var tz = Session.getScriptTimeZone() || "Europe/Berlin";
  var today = new Date();
  var startWin = atStartOfDay(addDays(today, -360), tz);
  var endWin   = atEndOfDay(addDays(today,  360), tz);

  // --- 1) Alle Zielkalender initialisieren ---
  var dsts = calendarIds.map(function(id){
    var c = CalendarApp.getCalendarById(id);
    if (!c) throw new Error("Zielkalender nicht gefunden: " + id);
    // Alte Script-Events löschen
    c.getEvents(startWin, endWin).forEach(function(ev){
      var d = ev.getDescription() || "";
      if (d.indexOf(marker) !== -1) { try { ev.deleteEvent(); } catch(_){} }
    });
    return c;
  });

  // --- 2) Geburtstage aus Kontakte lesen (einmalig) ---
  var birthdays = []; // Array von {name, month, day, year}
  var pageToken = null;
  do {
    var resp = People.People.Connections.list("people/me", {
      personFields: "names,birthdays,events",
      sources: ["READ_SOURCE_TYPE_CONTACT"],
      pageSize: 1000,
      pageToken: pageToken
    });
    var conns = (resp && resp.connections) ? resp.connections : [];
    conns.forEach(function(p){
      var name = (p.names && p.names.length && p.names[0].displayName) ? p.names[0].displayName : null;
      if (!name) return;
      var bd = pickBirthdayFromBirthdays(p.birthdays) || pickBirthdayFromEvents(p.events);
      if (!bd || !bd.month || !bd.day) return;
      birthdays.push({name:name, month:bd.month, day:bd.day, year:bd.year || null});
    });
    pageToken = resp && resp.nextPageToken ? resp.nextPageToken : null;
  } while (pageToken);

  // --- 3) In alle Kalender schreiben ---
  var createdTotal = 0;
  birthdays.forEach(function(bd){
    [today.getFullYear(), today.getFullYear() + 1].forEach(function(yr){
      var day = (bd.month === 2 && bd.day === 29 && !isLeap(yr)) ? 28 : bd.day;
      var when = new Date(yr, bd.month - 1, day);
      if (when < startWin || when > endWin) return;

      var title = "🎂 " + bd.name;
      if (bd.year) {
        var age = yr - bd.year;
        if (age >= 0 && age <= 150) title += " (" + age + ")";
      }

      dsts.forEach(function(c){
        try {
          c.createAllDayEvent(title, when, { description: marker });
          createdTotal++;
        } catch(_) {}
      });
    });
  });

  console.log("Geburtstage erstellt in " + calendarIds.length + " Kalender(n):", createdTotal);
}

Einrichtung für mehrere Kalender

In den Script Properties statt BIRTHDAY_CALENDAR_ID nun setzen:

→ Komma-getrennt, beliebig viele Kalender.

  • Trigger auf runSyncMultiple legen.