Home Assistant custom integration that reads the birthdays of your CardDAV contacts (iCloud, Radicale, Nextcloud, …) and exposes them as one calendar entity with yearly all-day events. Contacts stay the single source of truth; nothing has to be maintained twice.
Why this exists: iOS builds its "Birthdays" calendar locally from your contacts, so it is never served via CalDAV. The birthdays only exist in the contacts, which are reachable via CardDAV.
Inspired by nicklutt/ha-carddav-birthdays, which creates one sensor per contact. This is a separate implementation that creates a single calendar instead.
- One
calendar.birthdaysentity; works with the calendar card, thecalendar.get_eventsaction and calendar triggers - Addressbook discovery: enter the server root (e.g.
https://contacts.icloud.com), a direct addressbook URL also works - Event titles like
🎂 Erika Mustermann (35); no age if the contact has no birth year - Handles Apple's "birthday without year" placeholder (year 1604 /
X-APPLE-OMIT-YEAR),--MM-DD,YYYYMMDDand 29 February (shown on 1 March in non-leap years) - Reauth flow if the password is revoked; German and English UI
- No extra Python dependencies; refresh every 6 hours
Or manually:
- HACS → ⋮ (top right) → Custom repositories
- Repository:
https://github.com/max-t-d/HA_CardDAV_BDAY_Calendar, type: Integration → Add - Search for "CardDAV Birthday Calendar" in HACS, Download
- Restart Home Assistant
Updates then show up in HACS like any other integration.
Copy custom_components/carddav_bday_calendar into your
config/custom_components/ folder and restart.
Then go to Settings → Devices & services → Add integration → CardDAV Birthday Calendar.
- Server URL:
https://contacts.icloud.com - Username: your Apple Account email
- Password: an app-specific password (create one at account.apple.com → Sign-In and Security → App-Specific Passwords). Your normal password does not work.
automation:
- alias: Birthday reminder
triggers:
- trigger: calendar
event: start
entity_id: calendar.birthdays
offset: "9:00:00" # 09:00, the events start at midnight
actions:
- action: notify.notify
data:
message: "{{ trigger.calendar_event.summary }}"CardDAV has no push mechanism, so the integration polls:
- once when it is set up (Home Assistant start or reload), then every 6 hours
- changes to contacts (new birthday, edited date, deleted contact) therefore show up with a delay of up to 6 hours
- which birthday is "next" and the
on/offstate are calculated locally and roll over at midnight, independent of the polling and of server availability - if a refresh fails, the previous data is kept and the next attempt follows at the next interval; revoked credentials start the reauthentication flow
To refresh immediately, call the homeassistant.update_entity action on
calendar.birthdays (for example from a dashboard button) or reload the
integration under Settings → Devices & services.
The interval is fixed in custom_components/carddav_bday_calendar/const.py
(SCAN_INTERVAL).
- The entity state is
onon a day with a birthday; its attributes show the next birthday. - Only contacts with a parseable
BDAYappear. If an addressbook is large, the first refresh may take a moment.
The first fetch runs while the integration is being set up, so the calendar is filled immediately. If it stays empty:
- Check Settings → System → Logs for
carddav_bday_calendar. A warningNo birthdays found in addressbook …also tells you how many vCards the server returned. - For details, enable debug logging (Settings → Devices & services →
CardDAV Birthday Calendar → Enable debug logging), reload the integration
and look at the
REPORT (…) on …line: it shows how many vCards arrived, how many had a usable birthday, how many had noBDAYat all and how many were dropped as duplicate UIDs. Unreadable birthday formats are listed in a warning with all digits replaced by9, so no real dates or names end up in the log. - Make sure the contacts really have a birthday field and that you are looking at a month that contains one (the calendar view only shows the visible range).