<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:thr="http://purl.org/syndication/thread/1.0"
      xml:lang="en-us">
  <link rel="self" href="http://Andreas-Voit.at/feeds/blog-von-andreas-voit_tag_nextcloud.atom_1.0.links-and-content.xml" />
  <title type="text">Blog von Andreas Voit – tag: nextcloud</title>
  <id>https://andreas-voit.at/tags/nextcloud/</id>
  <link href="http://Andreas-Voit.at" />
  <icon>/favicon.ico</icon>
  <updated>2026-10-23T23:00:11+01:00</updated>
  <author>
    <name>Andreas Voit</name>
  </author>
  <subtitle type="text">Articles tagged with nextcloud from Blog von Andreas Voit</subtitle>
  <rights>All content written by Andreas Voit</rights>
  <generator uri='https://github.com/novoid/lazyblorg'>Generated from Org-mode source code using lazyblorg which is written in Python. Industrial-strength technology, baby.</generator>
<!-- ############################################################################################# -->
<entry>
    <title type="text">OSMCal-Termine mit Erinnerungen im Nextcloud-Kalender</title>
    <link href='https://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen' />
    <published>2026-10-23T17:30:00+01:00</published>
    <updated>2026-10-23T17:30:00+01:00</updated>
    <category scheme='https://Andreas-Voit.at/tags/nextcloud' term='nextcloud' />
    <category scheme='https://Andreas-Voit.at/tags/openstreetmap' term='openstreetmap' />
    <category scheme='https://Andreas-Voit.at/tags/docker' term='docker' />
    <category scheme='https://Andreas-Voit.at/autotags/language' term='language:deutsch' />
    <summary type='html'>
&lt;p&gt;

Auf &lt;a href="https://osmcal.org"&gt;OSMCal&lt;/a&gt; stehen die Termine der OpenStreetMap-Community: Stammtische, Mapathons, Online-Treffen und Konferenzen. Ich wollte diese Termine in meinem Nextcloud-Kalender haben, und zwar mit Erinnerungen, damit ich nicht erst am Abend selbst merke, dass heute Stammtisch gewesen wäre.

&lt;/p&gt;


&lt;p&gt;

In diesem Artikel zeige ich, wie ich das mit einem kleinen Python-Skript in einem Docker-Container gelöst habe. Es holt den Feed regelmäßig ab, hängt an jeden Termin mehrere Erinnerungen und schreibt alles per CalDAV in einen ganz normalen Nextcloud-Kalender. Das ganze läuft auf meinem ugreen NAS.

&lt;/p&gt;


&lt;figure class="image-center"&gt;
&lt;a href="http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots.png"&gt;
  &lt;img src="http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots%20-%20scaled%20width%20560.png" alt="Monatsansicht des Nextcloud-Kalenders mit OSMCal-Terminen" width="TMPREPLACEMEwidthSTART-2026-10-23T10.00.02 OSMCal Nextcloud Monatsansicht Demo -- screenshots - scaled width 560.png-TMPREPLACEMEwidthEND" height="TMPREPLACEMEheightSTART-2026-10-23T10.00.02 OSMCal Nextcloud Monatsansicht Demo -- screenshots - scaled width 560.png-TMPREPLACEMEheightEND" loading="lazy" /&gt;
&lt;/a&gt;
&lt;figcaption&gt;Beispielansicht: OSMCal-Termine (blau) neben eigenen Terminen im Nextcloud-Kalender &lt;a class="figcaption-clue-link" href="http://Andreas-Voit.at/2026/10/23/osmcal-nextcloud-erinnerungen/2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots.png"&gt;(klicken für größere Version)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

    </summary>    <content type='html'>

&lt;p&gt;

Auf &lt;a href="https://osmcal.org"&gt;OSMCal&lt;/a&gt; stehen die Termine der OpenStreetMap-Community: Stammtische, Mapathons, Online-Treffen und Konferenzen. Ich wollte diese Termine in meinem Nextcloud-Kalender haben, und zwar mit Erinnerungen, damit ich nicht erst am Abend selbst merke, dass heute Stammtisch gewesen wäre.

&lt;/p&gt;


&lt;p&gt;

In diesem Artikel zeige ich, wie ich das mit einem kleinen Python-Skript in einem Docker-Container gelöst habe. Es holt den Feed regelmäßig ab, hängt an jeden Termin mehrere Erinnerungen und schreibt alles per CalDAV in einen ganz normalen Nextcloud-Kalender. Das ganze läuft auf meinem ugreen NAS.

&lt;/p&gt;


&lt;figure class="image-center"&gt;
&lt;a href="2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots.png"&gt;
  &lt;img src="2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots%20-%20scaled%20width%20560.png" alt="Monatsansicht des Nextcloud-Kalenders mit OSMCal-Terminen" width="TMPREPLACEMEwidthSTART-2026-10-23T10.00.02 OSMCal Nextcloud Monatsansicht Demo -- screenshots - scaled width 560.png-TMPREPLACEMEwidthEND" height="TMPREPLACEMEheightSTART-2026-10-23T10.00.02 OSMCal Nextcloud Monatsansicht Demo -- screenshots - scaled width 560.png-TMPREPLACEMEheightEND" loading="lazy" /&gt;
&lt;/a&gt;
&lt;figcaption&gt;Beispielansicht: OSMCal-Termine (blau) neben eigenen Terminen im Nextcloud-Kalender &lt;a class="figcaption-clue-link" href="2026-10-23T10.00.02%20OSMCal%20Nextcloud%20Monatsansicht%20Demo%20--%20screenshots.png"&gt;(klicken für größere Version)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Die einfache Variante: Kalender-Abo&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

OSMCal bietet unter &lt;a href="https://osmcal.org/subscribe/"&gt;osmcal.org/subscribe&lt;/a&gt; einen iCal-Feed an. Für Feed-Reader gibt es auch einen RSS-Feed, aber den können Kalender nicht lesen. Der iCal-Feed lautet:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;https://osmcal.org/events.ics	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Der Feed lässt sich mit einem Ländercode filtern, zum Beispiel nur Termine aus Österreich mit &lt;code&gt;&lt;a href="https://osmcal.org/events.ics?in=AT&lt;/code&gt;."&gt;https://osmcal.org/events.ics?in=AT~.&lt;/a&gt;

&lt;/p&gt;


&lt;p&gt;

In Nextcloud bindet man ihn in der Kalender-App über „+ Neuer Kalender“ → „Neues Abonnement aus Link (nur lesen)“ ein. Das funktioniert sofort, hat aber zwei Nachteile:

&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Abonnierte Kalender sind &lt;strong&gt;schreibgeschützt&lt;/strong&gt;. An
einzelne Termine kann man keine Erinnerungen hängen.&lt;/li&gt;
&lt;li&gt;Nextcloud kann zwar pro Kalender eine Standarderinnerung setzen, das
ist aber &lt;strong&gt;nur eine&lt;/strong&gt; (getrennt nach ganztägigen und
anderen Terminen). Außerdem gilt sie nur für Termine, die man in der
Weboberfläche selbst neu anlegt.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;

Ich wollte aber mehrere Erinnerungen pro Termin: eine Woche vorher, zwei Tage vorher und einen Tag vorher. Deshalb die Variante mit Skript.

&lt;/p&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Die Idee&lt;/h2&gt;&lt;/header&gt;


&lt;figure class="image-center"&gt;
&lt;a href="2026-10-23T10.00.01%20OSMCal%20Nextcloud%20Ablauf%20--%20diagramme.png"&gt;
  &lt;img src="2026-10-23T10.00.01%20OSMCal%20Nextcloud%20Ablauf%20--%20diagramme%20-%20scaled%20width%20560.png" alt="Ablaufdiagramm von osmcal.org über osmcal-sync und Nextcloud zu Handy und Laptop" width="TMPREPLACEMEwidthSTART-2026-10-23T10.00.01 OSMCal Nextcloud Ablauf -- diagramme - scaled width 560.png-TMPREPLACEMEwidthEND" height="TMPREPLACEMEheightSTART-2026-10-23T10.00.01 OSMCal Nextcloud Ablauf -- diagramme - scaled width 560.png-TMPREPLACEMEheightEND" loading="lazy" /&gt;
&lt;/a&gt;
&lt;figcaption&gt;Ablauf: Feed abholen, Erinnerungen ergänzen, per CalDAV in Nextcloud schreiben &lt;a class="figcaption-clue-link" href="2026-10-23T10.00.01%20OSMCal%20Nextcloud%20Ablauf%20--%20diagramme.png"&gt;(klicken für größere Version)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;


&lt;p&gt;

Ein Container auf dem NAS holt alle sechs Stunden den iCal-Feed. Für jeden Termin baut er eine eigene Kalenderdatei mit den gewünschten Erinnerungen (&lt;code&gt;VALARM&lt;/code&gt;) und lädt sie per CalDAV in einen normalen Kalender „OSMCal“. Von dort synchronisieren Handy (DAVx⁵) und Laptop (Thunderbird) wie gewohnt.

&lt;/p&gt;


&lt;p&gt;

Dabei gilt:

&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neue Termine werden angelegt, geänderte Termine überschrieben.
Unveränderte Termine bleiben unangetastet, damit die Geräte nicht
ständig alles neu synchronisieren.&lt;/li&gt;
&lt;li&gt;Termine in der Zukunft, die aus dem Feed verschwinden (also abgesagt
wurden), werden gelöscht.&lt;/li&gt;
&lt;li&gt;Vergangene Termine bleiben stehen. Der Kalender wird so nebenbei zum
Archiv.&lt;/li&gt;
&lt;li&gt;Ist der Feed einmal leer, ändert das Skript gar nichts. So wird bei
einer Störung von OSMCal nicht versehentlich alles gelöscht.&lt;/li&gt;
&lt;li&gt;Den Kalender legt das Skript beim ersten Lauf selbst an.&lt;/li&gt;
&lt;/ul&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Voraussetzungen&lt;/h2&gt;&lt;/header&gt;

&lt;ul&gt;
&lt;li&gt;Nextcloud mit der Kalender-App (bei mir Nextcloud 34 in Docker)&lt;/li&gt;
&lt;li&gt;Ein Rechner mit Docker und Docker Compose, der Nextcloud erreicht.
Bei mir ist das dasselbe NAS.&lt;/li&gt;
&lt;li&gt;Ein Nextcloud-Benutzer, in dessen Konto der Kalender landen
soll&lt;/li&gt;
&lt;/ul&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 1: App-Passwort anlegen&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

Das Skript soll nicht mit dem richtigen Passwort arbeiten. In Nextcloud unter „Persönliche Einstellungen“ → „Sicherheit“ → „Neues App-Passwort erstellen“ legst du ein eigenes Passwort an, zum Beispiel mit dem Namen &lt;code&gt;osmcal-sync&lt;/code&gt;. Das kann man später jederzeit einzeln widerrufen.

&lt;/p&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 2: Projektordner anlegen&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

Auf dem NAS lege ich einen Ordner für das Docker-Projekt an. Bei mir liegen alle Dienste unter &lt;code&gt;/volume1/docker/&amp;lt;dienst&amp;gt;/&lt;/code&gt;:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;mkdir -p /volume1/docker/osmcal-sync
cd /volume1/docker/osmcal-sync	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Dort kommen vier Dateien hinein: das Skript, ein &lt;code&gt;Dockerfile&lt;/code&gt;, eine &lt;code&gt;docker-compose.yaml&lt;/code&gt; und eine &lt;code&gt;.env&lt;/code&gt; mit den Zugangsdaten.

&lt;/p&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 3: Das Skript&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

Die Datei &lt;code&gt;osmcal_sync.py&lt;/code&gt; braucht nur zwei Bibliotheken: &lt;code&gt;requests&lt;/code&gt; für HTTP und &lt;code&gt;icalendar&lt;/code&gt; zum Lesen und Schreiben der Kalenderdaten. CalDAV selbst ist bloß HTTP mit ein paar zusätzlichen Methoden (&lt;code&gt;PROPFIND&lt;/code&gt;, &lt;code&gt;REPORT&lt;/code&gt;, &lt;code&gt;MKCALENDAR&lt;/code&gt;), dafür braucht es keine eigene Bibliothek.

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;#!/usr/bin/env python3
"""OSMCal -&amp;gt; Nextcloud: holt den iCal-Feed von osmcal.org und schreibt jeden
Termin mit festen Erinnerungen per CalDAV in einen normalen Nextcloud-Kalender.

- Neue/geänderte Termine werden angelegt bzw. überschrieben.
- Zukünftige Termine, die aus dem Feed verschwinden (abgesagt), werden gelöscht.
- Vergangene Termine bleiben im Kalender stehen.
"""
import hashlib
import os
import re
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from urllib.parse import quote

import requests
from icalendar import Alarm, Calendar

FEED_URL = os.environ.get("OSMCAL_URL", "https://osmcal.org/events.ics")
NC_URL = os.environ["NC_URL"].rstrip("/")
NC_USER = os.environ["NC_USER"]
NC_PASS = os.environ["NC_PASS"]
CAL_SLUG = os.environ.get("NC_CALENDAR", "osmcal")
CAL_NAME = os.environ.get("NC_CALENDAR_NAME", "OSMCal")
# Erinnerungen als ISO-8601-Dauer vor Terminbeginn, kommagetrennt
ALARMS = [a.strip() for a in os.environ.get("ALARMS", "P7D,P2D,P1D").split(",") if a.strip()]
INTERVAL = int(os.environ.get("INTERVAL_HOURS", "6")) * 3600
PREFIX = "osmcal-"
# Zertifikatsprüfung: "true", "false" (selbstsigniert) oder Pfad zu einer CA-/Zertifikatsdatei
VERIFY = os.environ.get("NC_VERIFY_TLS", "true")
VERIFY = {"true": True, "false": False}.get(VERIFY.lower(), VERIFY)

CAL_URL = f"{NC_URL}/remote.php/dav/calendars/{quote(NC_USER)}/{CAL_SLUG}/"
NS = {"d": "DAV:", "c": "urn:ietf:params:xml:ns:caldav"}

http = requests.Session()
http.auth = (NC_USER, NC_PASS)
http.verify = VERIFY
if VERIFY is False:
    requests.packages.urllib3.disable_warnings()


def log(msg):
    print(f"{datetime.now():%Y-%m-%d %H:%M:%S} {msg}", flush=True)


def ensure_calendar():
    r = http.request("PROPFIND", CAL_URL, headers={"Depth": "0"})
    if r.status_code == 207:
        return
    if r.status_code != 404:
        r.raise_for_status()
    body = f"""&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
&amp;lt;c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"&amp;gt;
  &amp;lt;d:set&amp;gt;&amp;lt;d:prop&amp;gt;
    &amp;lt;d:displayname&amp;gt;{CAL_NAME}&amp;lt;/d:displayname&amp;gt;
    &amp;lt;c:supported-calendar-component-set&amp;gt;&amp;lt;c:comp name="VEVENT"/&amp;gt;&amp;lt;/c:supported-calendar-component-set&amp;gt;
  &amp;lt;/d:prop&amp;gt;&amp;lt;/d:set&amp;gt;
&amp;lt;/c:mkcalendar&amp;gt;"""
    r = http.request("MKCALENDAR", CAL_URL, data=body.encode(),
                     headers={"Content-Type": "application/xml; charset=utf-8"})
    r.raise_for_status()
    log(f"Kalender '{CAL_NAME}' angelegt")


def to_utc(value):
    """DTSTART (datetime oder date) als aware datetime in UTC."""
    if isinstance(value, datetime):
        if value.tzinfo is None:
            value = value.replace(tzinfo=timezone.utc)
        return value.astimezone(timezone.utc)
    return datetime(value.year, value.month, value.day, tzinfo=timezone.utc)


def existing_events():
    """href -&amp;gt; (Hash, Beginn) aller vom Skript angelegten Termine."""
    body = """&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;
&amp;lt;c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"&amp;gt;
  &amp;lt;d:prop&amp;gt;&amp;lt;c:calendar-data/&amp;gt;&amp;lt;/d:prop&amp;gt;
  &amp;lt;c:filter&amp;gt;&amp;lt;c:comp-filter name="VCALENDAR"&amp;gt;&amp;lt;c:comp-filter name="VEVENT"/&amp;gt;&amp;lt;/c:comp-filter&amp;gt;&amp;lt;/c:filter&amp;gt;
&amp;lt;/c:calendar-query&amp;gt;"""
    r = http.request("REPORT", CAL_URL, data=body.encode(),
                     headers={"Depth": "1", "Content-Type": "application/xml; charset=utf-8"})
    r.raise_for_status()
    result = {}
    for resp in ET.fromstring(r.content).findall("d:response", NS):
        href = resp.findtext("d:href", namespaces=NS) or ""
        name = href.rstrip("/").rsplit("/", 1)[-1]
        data = resp.findtext(".//c:calendar-data", namespaces=NS)
        if not name.startswith(PREFIX) or not data:
            continue
        ev = next(iter(Calendar.from_ical(data).walk("VEVENT")), None)
        if ev is None:
            continue
        result[name] = (str(ev.get("X-OSMCAL-HASH", "")), to_utc(ev.decoded("DTSTART")))
    return result


def build_objects(feed):
    """Dateiname -&amp;gt; (Hash, ics-Bytes) für jeden Termin im Feed."""
    timezones = {str(tz["TZID"]): tz for tz in feed.walk("VTIMEZONE")}
    objects = {}
    for ev in feed.walk("VEVENT"):
        uid = str(ev["UID"])
        name = PREFIX + re.sub(r"[^A-Za-z0-9_.-]", "_", uid) + ".ics"

        # DTSTAMP ändert sich bei jedem Abruf und zählt nicht als Änderung
        stamp = ev.pop("DTSTAMP", None)
        digest = hashlib.sha256(ev.to_ical() + ",".join(ALARMS).encode()).hexdigest()[:16]
        if stamp is not None:
            ev["DTSTAMP"] = stamp

        ev.subcomponents = [c for c in ev.subcomponents if c.name != "VALARM"]
        ev.add("X-OSMCAL-HASH", digest)
        for trigger in ALARMS:
            alarm = Alarm()
            alarm.add("ACTION", "DISPLAY")
            alarm.add("DESCRIPTION", str(ev.get("SUMMARY", "OSMCal-Termin")))
            alarm.add("TRIGGER", "-" + trigger, encode=False, parameters={"VALUE": "DURATION"})
            ev.add_component(alarm)

        cal = Calendar()
        cal.add("PRODID", "-//osmcal-sync//DE")
        cal.add("VERSION", "2.0")
        tzids = {p.params["TZID"] for p in (ev.get("DTSTART"), ev.get("DTEND"))
                 if p is not None and "TZID" in p.params}
        for tzid in sorted(tzids):
            if tzid in timezones:
                cal.add_component(timezones[tzid])
        cal.add_component(ev)
        objects[name] = (digest, cal.to_ical())
    return objects


def sync():
    r = requests.get(FEED_URL, timeout=60)
    r.raise_for_status()
    wanted = build_objects(Calendar.from_ical(r.content))
    if not wanted:
        log("Feed ist leer, nichts geändert (Schutz vor versehentlichem Löschen)")
        return

    ensure_calendar()
    have = existing_events()
    now = datetime.now(timezone.utc)
    created = updated = deleted = 0

    for name, (digest, ics) in wanted.items():
        if name in have and have[name][0] == digest:
            continue
        r = http.put(CAL_URL + name, data=ics, headers={"Content-Type": "text/calendar; charset=utf-8"})
        if r.status_code &amp;gt;= 400:
            log(f"Fehler {r.status_code} bei {name}: {r.text[:200]}")
            continue
        if name in have:
            updated += 1
        else:
            created += 1

    for name, (_, start) in have.items():
        if name not in wanted and start &amp;gt; now:
            http.delete(CAL_URL + name).raise_for_status()
            deleted += 1

    log(f"{len(wanted)} Termine im Feed: {created} neu, {updated} geändert, {deleted} gelöscht")


def main():
    once = "--once" in sys.argv
    while True:
        try:
            sync()
        except Exception as e:  # weiterlaufen, nächster Versuch im nächsten Intervall
            log(f"Fehler: {e}")
            if once:
                sys.exit(1)
        if once:
            break
        time.sleep(INTERVAL)


if __name__ == "__main__":
    main()	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Zwei Details, die mich beim Schreiben kurz beschäftigt haben:

&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OSMCal setzt bei jedem Abruf einen neuen Zeitstempel (&lt;code
class="verbatim"&gt;DTSTAMP&lt;/code&gt;). Würde das Skript den ganzen Termin
vergleichen, sähe jeder Termin bei jedem Lauf „geändert“ aus. Deshalb
wird für den Vergleich ein Hash ohne &lt;code
class="verbatim"&gt;DTSTAMP&lt;/code&gt; gebildet und als &lt;code
class="verbatim"&gt;X-OSMCAL-HASH&lt;/code&gt; im Termin gespeichert.&lt;/li&gt;
&lt;li&gt;Termine mit Uhrzeit haben im Feed eine Zeitzone (&lt;code
class="verbatim"&gt;TZID&lt;/code&gt;). Die passende &lt;code
class="verbatim"&gt;VTIMEZONE&lt;/code&gt; aus dem Feed wird mit in jede einzelne
Kalenderdatei kopiert, sonst landen die Termine eventuell zur falschen
Uhrzeit im Kalender.&lt;/li&gt;
&lt;/ul&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 4: Dockerfile und Compose&lt;/h2&gt;&lt;/header&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;FROM python:3.13-alpine
RUN pip install --no-cache-dir icalendar==7.3.0 requests==2.34.2
COPY osmcal_sync.py /app/osmcal_sync.py
RUN chmod -R a+rX /app
USER nobody
CMD ["python", "-u", "/app/osmcal_sync.py"]	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Der Container läuft als unprivilegierter Benutzer &lt;code&gt;nobody&lt;/code&gt;. Das &lt;code&gt;chmod&lt;/code&gt; ist kein Zufall, dazu weiter unten mehr.

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;services:
  osmcal-sync:
    build: .
    container_name: osmcal-sync
    restart: unless-stopped
    env_file: .env
    environment:
      TZ: Europe/Vienna	  &lt;/pre&gt;
    &lt;/div&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 5: Konfiguration in der .env&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

Alle Einstellungen stehen in der &lt;code&gt;.env&lt;/code&gt;. Die Adresse und der Benutzername hier sind Beispiele, die musst du an dein System anpassen:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;# Nextcloud (vom Docker-Host aus erreichbar)
NC_URL=http://192.168.0.10:8080
# Bei HTTPS mit selbstsigniertem Zertifikat: false
NC_VERIFY_TLS=true
NC_USER=benutzer
# App-Passwort aus Schritt 1
NC_PASS=xxxxx-xxxxx-xxxxx-xxxxx-xxxxx

# Kalender, der angelegt bzw. befüllt wird (Kürzel in der URL und Anzeigename)
NC_CALENDAR=osmcal
NC_CALENDAR_NAME=OSMCal

# Feed: weltweit. Nur Österreich: https://osmcal.org/events.ics?in=AT
OSMCAL_URL=https://osmcal.org/events.ics

# Erinnerungen vor Terminbeginn
ALARMS=P7D,P2D,P1D

# Abruf alle X Stunden
INTERVAL_HOURS=6	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Weil das App-Passwort darin steht, sollte nur root die Datei lesen dürfen:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;chmod 600 .env	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Die Erinnerungen werden als ISO-8601-Dauer angegeben. Dabei gibt es eine Falle: Für Stunden und Minuten braucht es ein &lt;code&gt;T&lt;/code&gt;, sonst sind Monate gemeint.

&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Angabe&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;P1W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 Woche vorher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;P7D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7 Tage vorher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;P1D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 Tag vorher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;PT2H&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 Stunden vorher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;PT15M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15 Minuten vorher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class="verbatim"&gt;P15M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15 &lt;em&gt;Monate&lt;/em&gt; vorher (!)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 6: Starten&lt;/h2&gt;&lt;/header&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;docker compose up -d --build
docker logs -f osmcal-sync	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

&lt;code&gt;docker logs -f&lt;/code&gt; verfolgt das Log laufend. Mit Strg+C beendest du nur die Anzeige, der Container läuft im Hintergrund weiter.

&lt;/p&gt;


&lt;figure class="image-center"&gt;
&lt;a href="2026-10-23T10.00.04%20OSMCal%20Sync%20Docker%20Log%20Demo%20--%20screenshots.png"&gt;
  &lt;img src="2026-10-23T10.00.04%20OSMCal%20Sync%20Docker%20Log%20Demo%20--%20screenshots%20-%20scaled%20width%20560.png" alt="Terminal mit docker compose up und docker logs osmcal-sync" width="TMPREPLACEMEwidthSTART-2026-10-23T10.00.04 OSMCal Sync Docker Log Demo -- screenshots - scaled width 560.png-TMPREPLACEMEwidthEND" height="TMPREPLACEMEheightSTART-2026-10-23T10.00.04 OSMCal Sync Docker Log Demo -- screenshots - scaled width 560.png-TMPREPLACEMEheightEND" loading="lazy" /&gt;
&lt;/a&gt;
&lt;figcaption&gt;Erster Start und die ersten Läufe im Log (Beispiel) &lt;a class="figcaption-clue-link" href="2026-10-23T10.00.04%20OSMCal%20Sync%20Docker%20Log%20Demo%20--%20screenshots.png"&gt;(klicken für größere Version)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;


&lt;p&gt;

Für einen einmaligen Testlauf ohne Endlosschleife gibt es &lt;code&gt;--once&lt;/code&gt;:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;docker compose run --rm osmcal-sync python /app/osmcal_sync.py --once	  &lt;/pre&gt;
    &lt;/div&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Schritt 7: Aufräumen und Geräte&lt;/h2&gt;&lt;/header&gt;

&lt;ul&gt;
&lt;li&gt;Wer vorher das schreibgeschützte Abo aus dem ersten Abschnitt
angelegt hat, sollte es jetzt löschen, sonst steht jeder Termin doppelt
im Kalender.&lt;/li&gt;
&lt;li&gt;In DAVx⁵ am Handy die Kalenderliste aktualisieren und „OSMCal“
anhaken. Thunderbird zeigt neue Kalender eines CalDAV-Kontos meist von
selbst an.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;

Danach hat jeder OSMCal-Termin seine Erinnerungen:

&lt;/p&gt;


&lt;figure class="image-center"&gt;
&lt;a href="2026-10-23T10.00.03%20OSMCal%20Nextcloud%20Termin%20mit%20Erinnerungen%20Demo%20--%20screenshots.png"&gt;
  &lt;img src="2026-10-23T10.00.03%20OSMCal%20Nextcloud%20Termin%20mit%20Erinnerungen%20Demo%20--%20screenshots%20-%20scaled%20width%20450.png" alt="Termindetails mit vier Erinnerungen" width="TMPREPLACEMEwidthSTART-2026-10-23T10.00.03 OSMCal Nextcloud Termin mit Erinnerungen Demo -- screenshots - scaled width 450.png-TMPREPLACEMEwidthEND" height="TMPREPLACEMEheightSTART-2026-10-23T10.00.03 OSMCal Nextcloud Termin mit Erinnerungen Demo -- screenshots - scaled width 450.png-TMPREPLACEMEheightEND" loading="lazy" /&gt;
&lt;/a&gt;
&lt;figcaption&gt;Ein OSMCal-Termin mit vier Erinnerungen (Beispiel) &lt;a class="figcaption-clue-link" href="2026-10-23T10.00.03%20OSMCal%20Nextcloud%20Termin%20mit%20Erinnerungen%20Demo%20--%20screenshots.png"&gt;(klicken für größere Version)&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;


&lt;p&gt;

Ändert man später &lt;code&gt;ALARMS&lt;/code&gt; in der &lt;code&gt;.env&lt;/code&gt;, reicht ein &lt;code&gt;docker compose up -d&lt;/code&gt;. Beim nächsten Lauf werden alle Termine mit den neuen Erinnerungen neu geschrieben, weil die Erinnerungen in den Hash einfließen.

&lt;/p&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Stolpersteine&lt;/h2&gt;&lt;/header&gt;


	  &lt;header&gt;&lt;h3 class="section-title"&gt;„Permission denied“ beim Start&lt;/h3&gt;&lt;/header&gt;


&lt;p&gt;

Mein erster Start endete in einer Endlosschleife aus:

&lt;/p&gt;


	  &lt;div class="example_code"&gt;
	  &lt;pre&gt;python: can't open file '/app/osmcal_sync.py': [Errno 13] Permission denied	  &lt;/pre&gt;
    &lt;/div&gt;


&lt;p&gt;

Die Datei lag auf einer NAS-Freigabe mit restriktiven Rechten. &lt;code&gt;COPY&lt;/code&gt; übernimmt diese Rechte ins Image, und der Benutzer &lt;code&gt;nobody&lt;/code&gt; im Container darf die Datei dann nicht lesen. &lt;code&gt;COPY --chmod=644&lt;/code&gt; hat das bei mir nicht behoben, erst ein eigenes &lt;code&gt;RUN chmod -R a+rX /app&lt;/code&gt; nach dem &lt;code&gt;COPY&lt;/code&gt; hat es gelöst.

&lt;/p&gt;


	  &lt;header&gt;&lt;h3 class="section-title"&gt;HTTP oder HTTPS?&lt;/h3&gt;&lt;/header&gt;


&lt;p&gt;

Mein Nextcloud ist von außen nur per HTTPS über Tailscale erreichbar. Das Skript spricht aber Nextcloud direkt im Heimnetz per HTTP an. Weil Skript und Nextcloud auf demselben NAS laufen, verlassen die Daten das Gerät nicht, und eine Komponente weniger kann ausfallen.

&lt;/p&gt;


&lt;p&gt;

Wer trotzdem HTTPS möchte:

&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bei einem selbstsignierten Zertifikat
&lt;code&gt;NC_VERIFY_TLS=false&lt;/code&gt; setzen.&lt;/li&gt;
&lt;li&gt;Bei einer Tailscale-Adresse (&lt;code class="verbatim"&gt;*.ts.net&lt;/code&gt;)
kann der Container den Namen meist nicht auflösen, weil MagicDNS nur im
Tailscale-Container aktiv ist. Ein &lt;code
class="verbatim"&gt;extra_hosts&lt;/code&gt;-Eintrag in der Compose-Datei mit der
Tailscale-IP des Hosts hilft.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;

Achtung bei Ugreen-NAS: Port 9443 ist dort die Verwaltungsoberfläche von UGOS, nicht Nextcloud.

&lt;/p&gt;


	  &lt;header&gt;&lt;h3 class="section-title"&gt;Eigene Änderungen an OSMCal-Terminen&lt;/h3&gt;&lt;/header&gt;


&lt;p&gt;

Man kann die Termine im Kalender bearbeiten, etwa eine zusätzliche Erinnerung setzen. Ändert sich der Termin aber später bei OSMCal, überschreibt das Skript die eigenen Änderungen.

&lt;/p&gt;


	  &lt;header&gt;&lt;h3 class="section-title"&gt;Doppelte Erinnerungen&lt;/h3&gt;&lt;/header&gt;


&lt;p&gt;

Die Erinnerungen zeigen Handy und Thunderbird an. Nextcloud selbst kann zusätzlich Push- oder E-Mail-Erinnerungen verschicken. Wer alles doppelt bekommt, schaltet eine der beiden Quellen ab.

&lt;/p&gt;


	  &lt;header&gt;&lt;h2 class="section-title"&gt;Fazit&lt;/h2&gt;&lt;/header&gt;


&lt;p&gt;

Ein Abo ist in zwei Minuten eingerichtet und für viele völlig ausreichend. Wer aber mehrere Erinnerungen pro Termin möchte, kommt mit einem kleinen Container und knapp 190 Zeilen Python ans Ziel. Ich hoffe zumindest für mich, dass ich so weniger oft Termine verpasse.

&lt;/p&gt;

    </content>
    <id>https://andreas-voit.at/2026/10/23/osmcal-nextcloud-erinnerungen-from-tag-feed-with-content</id>
</entry></feed>