A custom component for Home Assistant that imports TimeTree calendars through the
official TimeTree web API. Every calendar of your account becomes a real Home
Assistant calendar entity, so you can display it, create, change and delete
events from Home Assistant โ and optionally mirror everything into a second
calendar such as Google Calendar, with conflict handling that asks you
before something is lost.
| Document | Contents |
|---|---|
| ๐ Documentation home | which document for which situation, capability matrix of 1.1.3 vs. 2.0.x |
| ๐ Architecture Design Document | requirements, system context, components, 14 architectural decisions, risks |
| ๐งฉ Software Design Document | module inventory, interface contracts, data model, error matrix, test harness, traceability |
| ๐บ๏ธ Workflow Diagrams | GitDiagram repository map plus setup, polling, write, export and conflict flows |
| ๐๏ธ Design Documentation 1.1.3 (archived) | the previous architecture, plus the defect list that shaped 2.0.x |
| ๐ CHANGELOG | what changed in every release |
- All calendars of an account โ pick as many as you like during setup, each one becomes its own calendar entity (and its own device).
- Complete event import, including
- recurring events (expanded into single occurrences for the calendar card,
with
rrule/recurrence_idpreserved for clients), EXDATE/RDATEexceptions,COUNT/UNTILlimits (lunar rules fall back to the stored occurrence instead of breaking the view),- multi day and single day all-day events (TimeTree stores an inclusive end, Home Assistant expects an exclusive one โ both directions are converted),
- events before 1970 (negative timestamps),
- labels/colours, attendees and optional event comments,
memos, birthdays are skipped by default (option),- deleted events are dropped.
- recurring events (expanded into single occurrences for the calendar card,
with
- Chunked sync โ the
events/synccursor is followed until the last page, so large calendars are imported completely. - Diagnostic sensors for the last successful sync, conflict count, last export and export status.
calendar.create_event,calendar.update_event/calendar/event/updateandcalendar.delete_event/calendar/event/deleteare supported for every TimeTree calendar entity.- The full browser payload is sent (label, attendees, attachment, alerts,
recurrences), the CSRF token is scraped from the event editor page and the
requests use a browser shaped TLS fingerprint through
curl_cffiโ the TimeTree API rejects plainrequestsclients with a generic422. - When
curl_cffiis unavailable the integration still works for reading and logs a warning; writes may then be refused by TimeTree's anti bot layer.
- Export to any Home Assistant calendar entity, e.g. the Google Calendar integration. TimeTree stays the source of truth.
- Exported copies carry an invisible marker (
[timetree:<uuid>]) at the end of their description, which is how the pair is matched again later. - Configurable loop interval (5โ1440 min), export window (past/future days),
export_onlyortwo_way, dry-run mode, and optional import of events that only exist in the target calendar. - Attendee filter โ restrict the export to events that include at least one of the selected members (discovered from your calendars), with a switch for events that carry no attendee at all.
- Recurring events are exported as a series (
RRULE), so Google keeps expanding them natively.
A conflict is raised whenever a change cannot be applied without losing data. Typical cases:
| Conflict | Meaning |
|---|---|
both_changed |
The TimeTree event and its exported copy changed |
target_changed |
Only the exported copy changed (export-only mode) |
target_removed |
The exported copy was deleted or moved out of the window |
source_removed |
The TimeTree event was deleted, but the copy changed |
Conflict policies: manual (default, queue it), source_wins, target_wins,
newest_wins. With manual nothing is written, a notification is created, the
sensor.<calendar>_sync_conflicts sensor lists the pending conflicts, the
timetree_conflict_detected event fires and you decide per conflict:
| Resolution | Effect |
|---|---|
use_timetree |
Write the TimeTree version to the export target (or delete the copy) |
use_export_target |
Write the copy back into TimeTree (or delete the TimeTree event) |
use_newest |
Pick the side with the newer modification timestamp |
skip |
Keep both versions and accept the divergence as the new baseline |
ignore_forever |
Exclude this event from synchronisation permanently |
- HACS โ Integrations โ โฎ โ Custom repositories.
- Add
https://github.com/acdcnow/Timetree-Import-for-Home-Assistant, category Integration. - Install TimeTree Calendar, restart Home Assistant.
- Download the latest release.
- Copy
custom_components/timetreeinto/config/custom_components/. - Restart Home Assistant.
Home Assistant 2026.9 or newer is required. The integration ships its own brand images in
custom_components/timetree/brand/, so no separate brand PR is needed.
- Settings โ Devices & Services โ + Add integration โ TimeTree Calendar.
- Enter the e-mail address and password of your TimeTree account.
- Select all calendars you want in Home Assistant.
Each calendar becomes a device with a calendar entity
(calendar.<calendar name>) and a Last updated diagnostic sensor.
Settings โ Devices & Services โ TimeTree Calendar โ Configure opens a menu:
| Menu | Options |
|---|---|
| Calendars | add/remove TimeTree calendars (entities are added/removed) |
| Polling and events | poll interval (5โ1440 min), include birthdays, include comments, labels/attendees in the description |
| Export to another calendar | enable, target entity, interval, direction, window, attendee filter, delete/recreate behaviour, dry run, import unmanaged events |
| Conflict handling | policy and notifications |
service: calendar.create_event
target:
entity_id: calendar.family
data:
summary: "Family Dinner"
description: "Pizza night!"
location: "Home"
start_date_time: "2026-12-31 18:00:00"
end_date_time: "2026-12-31 20:00:00"All-day events use start_date/end_date (Home Assistant's end date is
exclusive, TimeTree's is inclusive โ the conversion happens automatically).
action:
- service: timetree.export_now
data:
dry_run: true # show what would happen firstA typical setup:
- Add your Google calendar to Home Assistant (Google Calendar integration) and make sure the entity is not read-only.
- Configure โ Export to another calendar: enable the loop, pick
calendar.google, choose an interval (e.g. every 60 minutes) and a window (e.g. 7 days back / 90 days forward). - Run
timetree.export_nowonce with dry run enabled to see the result.
Export marks only touch the exported copies. Deleting an exported copy in the
target will create a target_removed conflict (or recreate it, if
Recreate deleted exported copies is enabled).
# inspect
- service: timetree.list_conflicts
response_variable: pending
# decide
- service: timetree.resolve_conflict
data:
conflict_id: "{{ pending.conflicts[0].conflict_id }}"
action: use_timetreeOr apply one decision to everything:
timetree.resolve_all_conflicts with action: skip.
| Service | Response | Description |
|---|---|---|
timetree.refresh_now |
optional | poll TimeTree immediately (config_entry_id optional) |
timetree.export_now |
optional | run the export/sync now (dry_run supported) |
timetree.list_conflicts |
only | every pending conflict with both versions |
timetree.resolve_conflict |
optional | resolve one conflict (conflict_id, action) |
timetree.resolve_all_conflicts |
optional | apply action to every pending conflict |
timetree.get_event |
only | raw TimeTree details of one event (uuid) |
| Event | Payload |
|---|---|
timetree_export_completed |
counters of the run (created, updated, deleted, imported, conflicts, errors, dry_run) |
timetree_conflict_detected |
conflict_id, kind, summary, calendar_id, timetree_uuid, message |
| Symptom | Cause / fix |
|---|---|
invalid_auth, error code -702 |
TimeTree temporarily blocked the sign-in (too many attempts). Wait a few minutes and use Re-authenticate. |
Writes fail with 422 / failed to api request |
TimeTree's WAF rejected the request. Make sure curl_cffi was installed (check Diagnostics โ export โ browser_transport) and that the calendar has an alias_code (reload the integration). |
| Events missing in a month view | Fixed in 2.0.0: RRULE: prefixes are stripped before handing rules to Home Assistant. |
| All-day events show one day too long/short | Fixed in 2.0.0: TimeTree's inclusive end is converted to Home Assistant's exclusive end (and vice versa on write). |
create_event fails with combine() argument 1 must be datetime.date, not None |
Fixed in 2.0.0: the entity now reads the dtstart/dtend keys that Home Assistant actually sends. |
Could not reach the TimeTree API |
Check the internet connection; the integration uses timetreeapp.com. |
| Conflicts pile up | Set a policy that matches your workflow (source_wins) or resolve them in bulk. |
Verbose logging:
logger:
default: info
logs:
custom_components.timetree: debugDiagnostics (Settings โ Devices & Services โ TimeTree โ Download diagnostics) contain the parsed options, per calendar statistics, the sync store state and the pending conflicts โ with credentials, e-mail and the password redacted.
The full design is documented in the wiki: the Architecture Design Document (decisions and constraints), the Software Design Document (modules, contracts, data model, verification) and the Workflow Diagrams.
| Purpose | Endpoint |
|---|---|
| Login | PUT /api/v1/auth/email/signin (_session_id cookie) |
| Own user id | GET /api/v1/user |
| Calendars + members | GET /api/v1/calendars?since=0 |
| Labels | GET /api/v1/calendar/{id}/labels |
| Events | GET /api/v1/calendar/{id}/events/sync[?since=] (chunked) |
| Comments | GET /api/v1/calendar/{id}/event/{uuid}/activities?since=0 |
| Create | POST /api/v1/calendar/{id}/event |
| Update | PUT /api/v1/calendar/{id}/event/{uuid} |
| Delete | DELETE /api/v1/calendar/{id}/event/{uuid} |
Notes:
POST/PUT/DELETEneed a freshX-CSRF-Token, which is scraped fromGET /calendars/{alias_code}/events/new, plus browser shaped headers (Origin,Referer,Sec-Fetch-*) and a browser TLS fingerprint โ hencecurl_cffi. The event resource is singular (/event, not/events).- TimeTree stores all-day events as UTC midnights with an inclusive end date;
a single day event therefore has
start_at == end_at. - Update/delete use the singular resource first and fall back to the plural one, which keeps them working if TimeTree changes the route again.
- TimeTree is a cloud service: reading and writing require internet access and are subject to TimeTree's rate limits.
- A single occurrence of a recurring event cannot be edited or deleted through the API; TimeTree only knows whole series. Home Assistant gets a clear error in that case instead of silently touching the series.
use_newestcan only compare modification timestamps that both sides expose. TimeTree provides one, most calendar targets do not, so ambiguous cases still end up as a conflict.- The importer for events that only exist in the target calendar (Import unmanaged events) is opt-in and matches new TimeTree events through the marker it writes back into the target copy.
- The integration uses TimeTree's internal API. A backend change can break it.
Huge thanks to eoleedi for TimeTree-Exporter. Its reverse-engineered API handling and data structures are the foundation of this integration โ without that project none of this would exist.
Also thanks to everyone who reported the issues that shaped 2.0.0
(#1โ#6) and to ratzepumml for the recurring
event analysis in #7.
GNU General Public License v3.0 โ see LICENSE. This program comes with absolutely no warranty.
