Repository navigation
Architecture Design Document
Applies to integration 2.0.x · branch
ha2026_09_dev· version2.0.0-beta.2· Home Assistant 2026.9+Supersedes Design Documentation 1.1.3 (Archived). Companion documents: Software Design Document · Workflow Diagrams · Documentation home.
This document explains the architecture of the TimeTree integration: the forces that shaped it, the components it is made of, the decisions that were taken and the alternatives that were rejected. It is the reference for anyone who maintains, reviews or extends the integration.
- The integration inside
custom_components/timetree/— everything from the config flow to the export loop. - The contract with Home Assistant (config entries, entities, services, diagnostics, storage).
- The contract with the TimeTree web API (endpoints, payloads, authentication, anti-bot measures).
- The synchronisation model between TimeTree, Home Assistant and an optional second calendar ("export target") including conflict handling.
- TimeTree's own backend behaviour, rate limits and terms of service.
- The implementation of other integrations used as export targets (Google Calendar, local calendar, another TimeTree account, …).
- Card/frontend behaviour in Home Assistant (the integration only produces
CalendarEventobjects). - Hardware, HAOS installation and HACS packaging details (see the README).
| Stakeholder | Interest |
|---|---|
| Home Assistant user | see and manage the real TimeTree schedule in Home Assistant; keep a second calendar (Google) in sync without losing data |
| Maintainer | a codebase that survives TimeTree API drift and Home Assistant releases; automated verification without a live HA instance |
| Home Assistant core | an integration that follows the config-entry, entity, service and diagnostics conventions of 2026.9 |
| TimeTree | an API client that behaves like the official web client and does not hammer the backend |
| ID | Requirement | Where it is implemented |
|---|---|---|
| FR-1 | Import every event of every selected TimeTree calendar, including all chunked pages |
api.TimeTreeApi._get_events, coordinator.TimeTreeCoordinator
|
| FR-2 | Expose each calendar as a selectable calendar.* entity with its own device |
calendar.TimeTreeCalendarEntity, entity.py, __init__.py
|
| FR-3 | Create, update and delete events from Home Assistant |
calendar.py (entity API), api.py (write endpoints) |
| FR-4 | Handle recurring events: expand occurrences per query range, keep the series rule | recurrence.py |
| FR-5 | Handle all-day events with correct inclusive/exclusive semantics in both directions |
models.py (ha_end, build_event_payload) |
| FR-6 | Enrich events with labels, colours, attendees, locations, URLs and optional comments |
api.py, models.py, calendar.py
|
| FR-7 | Export events into a second calendar on a selectable schedule |
export.ExportManager, config_flow.py
|
| FR-8 | Detect divergence between TimeTree and the exported copy and queue a conflict when data would be lost |
conflicts.decide, export.ExportManager
|
| FR-9 | Let a human resolve a conflict (either side, newest, skip, ignore forever) |
export.ExportManager.async_resolve_conflict, services.py
|
| FR-10 | Persist sync state across restarts | store.TimeTreeSyncStore |
| FR-11 | Provide diagnostics and status entities for troubleshooting |
diagnostics.py, sensor.py
|
| FR-12 | Support multiple accounts/entries and multiple calendars per entry | config flow, options flow, per-calendar devices |
| ID | Requirement | Target |
|---|---|---|
| NFR-1 | Home Assistant compatibility | 2026.9+ (entry.runtime_data, ConfigFlowResult, device registry API) |
| NFR-2 | No blocking work in the event loop | every HTTP call and every recurrence expansion runs in an executor |
| NFR-3 | Bounded work per refresh |
MAX_OCCURRENCES = 1000, MAX_EXPANSION_DAYS = 1098, chunked sync aborts after 100 pages |
| NFR-4 | Secrets never leak | credentials redacted in diagnostics and never logged; debug logs carry payloads only at debug level |
| NFR-5 | Failure isolation | one broken event/calendar must not break a refresh or an export run |
| NFR-6 | Idempotent exports | repeated runs must not duplicate events |
| NFR-7 | Dependency policy | only what Home Assistant does not already provide: curl_cffi, python-dateutil
|
| NFR-8 | Testability without HA | HA-free core modules plus a stub harness (tests/) |
| NFR-9 | Language support | all user visible strings translated (strings.json, translations/en.json, translations/de.json) |
TimeTree is one of the most widely used shared calendar apps, especially for families, but it has no public API and no Home Assistant integration. Users who want their family calendar on a dashboard — and who additionally keep a Google Calendar for other purposes — had no way to see TimeTree in Home Assistant, let alone to write into it or keep two calendars aligned.
The integration closes that gap by acting as the official web client: it signs in with the user's e-mail and password, speaks the same internal JSON API the web app uses, and behaves like a normal Home Assistant calendar entity.
| Constraint | Consequence |
|---|---|
No public API; TimeTree's internal https://timetreeapp.com/api/v1 is used |
endpoints can change without notice → defensive parsing, endpoint fallbacks, extensive debug logging |
Session cookie _session_id from PUT /auth/email/signin
|
sessions expire → transparent re-login on 401
|
| Write endpoints sit behind a WAF that fingerprints the TLS client hello | plain requests gets a generic 422 → curl_cffi with impersonate="firefox135", browsery headers, scraped X-CSRF-Token
|
| Home Assistant expects expanded occurrences with an exclusive end | the integration owns expansion and both day-boundary conversions |
| Exported copies must be recognisable in a foreign calendar | marker [timetree:<uuid>] appended to the description |
| Export targets differ in capability (Google: create + delete, local calendar: create + update + delete) | capability detection on the target entity and a create-before-delete fallback |
Wi-Fi/pre-1970/negative timestamps and ZoneInfo availability |
epoch arithmetic instead of fromtimestamp, get_zone() fallback to UTC |
flowchart LR
subgraph users["People"]
user(("Home Assistant user"))
automation(("Automation / script"))
end
subgraph ha["Home Assistant runtime"]
subgraph tt["custom_components/timetree (2.0.x)"]
flow["Config & options flow"]
coord["TimeTreeCoordinator"]
cal["Calendar entities (one per calendar)"]
sens["Sensors: last updated, conflicts, last export, export status"]
exp["ExportManager (loop + conflict engine)"]
store[("Sync store<br/>timetree.sync.entry_id.json")]
end
core["HA core: config entries, entity/device registry,<br/>services, event bus, dispatcher, persistent notification"]
target["Export target entity<br/>(e.g. Google Calendar)"]
end
ttcloud{{"TimeTree web API<br/>timetreeapp.com/api/v1"}}
google{{"Google Calendar"}}
user -->|"setup, options, services"| flow
automation -->|"calendar.create_event / get_events"| cal
user -->|"resolve_conflict, export_now"| exp
flow --> coord
coord -->|"REST + cookie session"| ttcloud
cal -->|"REST writes (curl_cffi)"| ttcloud
coord --> cal
coord --> sens
exp -->|"get_events / create / update / delete"| target
exp -->|"write back (two_way)"| coord
exp <--> store
target -->|"Google sync"| google
tt -.-> core
| Group | Components | Responsibility |
|---|---|---|
| Configuration |
config_flow.TimeTreeConfigFlow, config_flow.TimeTreeOptionsFlow, options.TimeTreeOptions
|
credentials, calendar selection, all tunables, legacy 1.x layout migration |
| Integration lifecycle |
__init__.py (runtime data, hub device, platforms, services, options listener), services.py
|
wiring, reload behaviour, service surface |
| Transport |
api.TimeTreeApi, transport factory, CSRF helper, error taxonomy |
authentication, session handling, read and write endpoints, rate-limit friendly logging |
| Domain model |
models.py, recurrence.py, conflicts.py
|
parsing, all-day and timezone semantics, occurrence expansion, change detection and the decision table |
| Orchestration |
coordinator.TimeTreeCoordinator, coordinator.TimeTreeData / CalendarData
|
polling, per calendar snapshots, write passthrough with refresh |
| Presentation |
entity.py, calendar.py, sensor.py
|
devices, calendar entity (read/write), status and conflict sensors |
| Synchronisation |
export.ExportManager, export.CalendarTarget, export.TargetWrite, store.TimeTreeSyncStore
|
export loop, target adapter, marker pairing, conflict queue, persisted state, re-baseline |
| Operational |
diagnostics.py, strings.json, translations/*, brand/*
|
supportability, localisation, branding |
flowchart TD
subgraph config["Configuration"]
cf["Config / options flow"]
opt["TimeTreeOptions"]
end
subgraph runtime["Orchestration"]
init["__init__ / runtime data"]
coord["TimeTreeCoordinator"]
svc["services.py"]
end
subgraph domain["Domain (HA free)"]
models["models.py"]
rec["recurrence.py"]
conf["conflicts.py"]
end
subgraph net["Transport"]
api["TimeTreeApi"]
end
subgraph present["Presentation"]
ent["entity.py"]
cal["calendar.py"]
sen["sensor.py"]
end
subgraph sync["Synchronisation"]
exp["ExportManager"]
tgt["CalendarTarget"]
store["TimeTreeSyncStore"]
end
tt{{"TimeTree API"}}
oth{{"Target calendar"}}
cf --> opt --> init
init --> coord
init --> exp
init --> svc
coord --> api
api --> tt
api --> models
models --> rec
coord --> cal
ent --> cal
coord --> sen
cal --> api
cal --> rec
exp --> coord
exp --> tgt --> oth
exp --> conf
exp <--> store
svc --> exp
svc --> coord
Context. 1.1.3 created one config entry per calendar, duplicating credentials and sessions. Users with a family account have three or more calendars.
Decision. One config entry holds the account (email/password) and a list of selected
calendars (CONF_CALENDARS); every calendar gets its own calendar.* entity, its own
device and its own diagnostic sensor. Options can add or remove calendars at any time.
Rationale. One login, one API session, one coordinator fetch pass for N calendars; the
user picks calendars once. unique_id is the e-mail address, which prevents double setup.
Consequences. Entity ids are derived from the calendar name/device, so they change relative to 1.1.3. Removing a calendar in the options flow removes its entity and device.
Alternatives rejected. (a) Keep one entry per calendar — duplicated credentials and sessions; (b) a "sub-entry" style design — not supported by HA.
Context. There is no public, documented TimeTree API. The web client API is the only
option and it changes occasionally (for example the events → event write route).
Decision. All HTTP knowledge lives in api.py, spoken through one TimeTreeApi class.
Parsing never happens there; it is delegated to models.py, which is Home-Assistant-free.
Rationale. API drift can be absorbed inside one file; the domain model stays testable.
Consequences. Endpoint changes require a release, but the fallback route list
(/event/{uuid} → /events/{uuid}) softens that.
Context. TimeTree's write endpoints are behind a WAF that checks the TLS fingerprint
(issue #5): the identical request fails with requests/curl and succeeds with
curl_cffi impersonating Firefox.
Decision. The transport is built by create_transport(): prefer
curl_cffi.requests.Session(impersonate="firefox135"), fall back to requests.Session.
Writes additionally send Origin, Referer, Sec-Fetch-* and a scraped X-CSRF-Token.
browser_transport is exposed in diagnostics so support can see which stack is active.
Rationale. Reads work on both stacks, so the cheap fallback keeps the integration usable
even if curl_cffi cannot be installed; writes degrade with a clear log warning and a
readable error instead of a mysterious 422.
Consequences. A native dependency ships with the integration; the impersonation profile
must be kept in sync with curl_cffi releases (documented risk R-1).
Context. 1.1.3 kept everything in hass.data; the synchronisation requirements
(fingerprints, target uids, conflicts) need to survive restarts and reloads.
Decision. TimeTreeSyncStore wraps homeassistant.helpers.storage.Store with the key
timetree.sync.<entry_id> and a versioned schema (records, conflicts, meta), written
with async_delay_save(…, 10) and async_save_now() for conflict decisions.
Rationale. HA's store handles atomic writes and versioning; a debounce avoids a write per event; the data stays local (no cloud sync).
Consequences. The store can grow with the number of managed events; records are dropped when an event disappears on both sides.
Context. calendar.create_event returns nothing usable, targets generate their own ids,
and Google's ical_uuid cannot be set through the service API.
Decision. Every exported copy carries [timetree:<uuid>] at the end of its description.
On each run the target window is indexed by that marker; the TimeTree uuid is the single
identity key across records, markers and conflicts. A discovered target uid is stored
opportunistically for update/delete calls.
Rationale. It works for every target that round-trips description (Google does),
needs no extra API, and survives restarts.
Consequences. The description of exported copies contains a marker (documented in the README). Targets that rewrite descriptions lose the pairing (documented limitation).
Context. Neither Home Assistant's CalendarEvent nor most targets expose a reliable
modification timestamp. "Who changed last?" cannot be answered generically.
Decision. EventState projects the synchronised fields (summary, description, location,
start, end, all_day, rrule) into a stable SHA-256 fingerprint; the store keeps the
fingerprints of the last synced state per side. decide() derives source-changed and
target-changed by comparison.
Rationale. Deterministic, independent of clock skew and target metadata; works for any target.
Consequences. newest_wins can only use the TimeTree updated_at (see AD-7).
Context. A one-way export is easy; the moment the user edits the exported copy, silent overwrites lose data.
Decision. decide() returns a Decision; ambiguous cases become a Conflict that is
stored, surfaced (sensor attributes, persistent notification,
timetree_conflict_detected event) and resolved manually through
timetree.resolve_conflict with use_timetree, use_export_target, use_newest, skip
or ignore_forever. Policies (manual, source_wins, target_wins, newest_wins) allow
automation for users who do not want to be asked.
Rationale. "Never lose data silently" is the requirement that drove the feature. A queued conflict plus a visible sensor is more honest than last-write-wins.
Consequences. A user who ignores conflicts accumulates them; skip re-baselines,
ignore_forever excludes the event permanently.
Context. Home Assistant renders exactly the CalendarEvent list an integration returns
for a requested range. 1.1.3 returned one event with a malformed rule, which broke every
view (issue #2).
Decision. recurrence.py expands occurrences for the requested window with
dateutil.rrule/rruleset (supporting EXDATE, RDATE, COUNT, UNTIL, all-day
series), sets recurrence_id per occurrence and hands the bare rule to Home Assistant
for the master event. Unsupported (lunar) rules fall back to the stored occurrence.
Rationale. Matches Home Assistant core (local_calendar materialises a timeline and
returns occurrences), keeps dashboards correct and never breaks a whole view because of one
odd rule.
Consequences. Expansion runs in an executor and is bounded (MAX_OCCURRENCES,
MAX_EXPANSION_DAYS).
Context. TimeTree stores all-day events as UTC midnights with an inclusive end (a one
day event has start_at == end_at); Home Assistant expects an exclusive end (issue #5,
both directions).
Decision. TimeTreeEvent.ha_end returns the exclusive end (+1 day), and
build_event_payload() converts back (-1 day, UTC midnight, start_timezone = "UTC").
Rationale. One place for the conversion keeps read, write and export consistent and matches the captured web-client payload.
Consequences. Exported all-day copies use Home Assistant's exclusive convention, which is correct for Google.
Context. The requirement names Google Calendar, but Home Assistant has many calendar
entities with different feature sets (UPDATE_EVENT is optional).
Decision. The options flow selects any calendar.* entity. CalendarTarget wraps the
entity object, detects CREATE_EVENT/UPDATE_EVENT/DELETE_EVENT, and, when a target
cannot update in place, creates the replacement first and deletes the old copy
afterwards.
Rationale. One implementation serves Google, local calendars, other TimeTree accounts and future integrations. Create-before-delete means a failed write never loses data.
Consequences. Since Google only supports create/delete, one event change costs two API calls. Deletion failures are logged, not fatal.
Context. Targets normalise what we write (line breaks, extra properties) and TimeTree adjusts timestamps. Assuming "what we sent is what they store" produced false conflicts.
Decision. After a run with writes, _async_rebaseline() refreshes the coordinator and
re-reads the target window, then stores the observed fingerprints/uids for the touched
events.
Rationale. The next run compares real state with real state; false "both changed" conflicts disappear.
Consequences. One extra read per side per changed run (only when something changed).
Context. Event comments require one request per event; recurrence expansion can explode.
Decision. include_comments is off by default and fetched with a bounded thread pool
(MAX_COMMENT_WORKERS = 4); expansion is capped; the export window is configurable
(past/future days, MAX_EXPORT_DAYS = 730).
Rationale. A refresh must stay fast and must not trigger TimeTree rate limits.
Context. Users need to trigger an export, inspect conflicts and decide about them.
Decision. Six services (refresh_now, export_now, list_conflicts,
resolve_conflict, resolve_all_conflicts, get_event) with voluptuous schemas, response
support where it is useful (SupportsResponse.ONLY|OPTIONAL) and services.yaml plus
translations. Two bus events (timetree_export_completed,
timetree_conflict_detected) allow automations to react.
Rationale. Everything the integration does can also be observed and driven from
automations; nothing requires editing files in .storage.
Context. HA 2026.8/2026.9 deprecated several APIs (hass.data for integration state,
via_device, default_name, merge_identifiers, async_get_device, the OptionsFlow
constructor taking the entry).
Decision. Use entry.runtime_data, ConfigFlowResult, DeviceInfo with
identifiers/via_device_id, dr.async_get_or_create, AddConfigEntryEntitiesCallback
and an OptionsFlow that reads self.config_entry. The manifest declares
integration_type, iot_class, issue_tracker, loggers, homeassistant: 2026.9.0.
Rationale. The 1.1.3 code did not run on 2026.9 without these changes (issues #1, #3).
Consequences. Hard minimum version 2026.9 — users on older HA must stay on 1.1.3.
| Layer | Strategy |
|---|---|
Transport (api.py) |
error taxonomy TimeTreeError → TimeTreeAuthError / TimeTreeApiError / TimeTreeWriteError; 401 triggers one transparent re-login; -702 (Temporarily blocked) becomes a readable auth message |
| Coordinator |
ConfigEntryAuthFailed for credential problems (HA starts the reauth flow), UpdateFailed for transient errors, per-calendar error collection so one broken calendar does not hide the others |
| Entities |
HomeAssistantError with an actionable text for create/update/delete; occurrence-level edits of recurring events are refused explicitly |
| Export loop | per-event try/except (one bad event never aborts the run), report carries errors, run state exposed in the export status sensor |
| Persistence | store writes are delayed/debounced; a corrupt store is reported by diagnostics rather than crashing setup |
| Dependency | Why it is needed | Why it is acceptable |
|---|---|---|
curl_cffi>=0.7.0 |
writes need a browser TLS fingerprint (WAF) | reads fall back to requests; the fallback is logged and visible in diagnostics |
python-dateutil>=2.8.2 |
recurrence expansion (rrule, rruleset) |
same library Home Assistant core uses in its calendar component |
requests |
fallback transport | provided by Home Assistant core |
-
debugoncustom_components.timetreeshows login attempts, payloads, endpoint paths, conflict decisions and marker matches — never credentials. - Warnings are reserved for actionable problems (write rejected, conflict detected, some calendars failed, target not found).
- The export run always records an
ExportReport(counters + errors) in the store and in thetimetree_export_completedevent.
- Credentials live only in the config entry (HA encrypted storage); they are never logged and
are redacted in diagnostics (
async_redact_data). - The session cookie is held in memory by the API client.
- The sync store contains event metadata (titles/times/fingerprints), not credentials.
- The marker
[timetree:<uuid>]is the only TimeTree specific content written into a foreign calendar.
- Timestamps are converted with epoch arithmetic (
EPOCH + timedelta) so negative values (pre-1970) work everywhere. - Timed events keep the timezone of the TimeTree event; Home Assistant converts to the local zone for display.
- All-day events are UTC midnights; the date is derived with the event's timezone and the end is exclusive on the Home Assistant side.
-
get_zone()degrades to UTC when a zone is unknown (for example on hosts without a tz database) instead of raising.
- Exports are idempotent: matching is by uuid marker, and unchanged fingerprints produce
Action.NONE. - The sync cursor chain is followed to the end but aborts after 100 pages.
- Expansion, comment fetching, export windows and conflict lists are all bounded.
| Area | 1.1.3 | 2.0.x |
|---|---|---|
| Modules | 7 (api, calendar, config_flow, const, coordinator, sensor, __init__) |
15 (+ models, recurrence, conflicts, export, store, services, options, entity, diagnostics, strings.json, services.yaml, brand/, translations) |
| Calendars per entry | 1 | N (multi-select, options flow) |
| Event source | first page of events/sync only |
full cursor chain |
| Recurrence | raw RRULE: string handed to HA → view broke |
expanded occurrences + bare rule + EXDATE/RDATE
|
| All-day | off by one in both directions | inclusive ⇄ exclusive conversion, UTC midnights |
| Write | create only, wrong kwargs, wrong route, 422
|
create/update/delete, browser payload, CSRF, curl_cffi, route fallback |
| State | hass.data[DOMAIN][entry_id] |
entry.runtime_data dataclass |
| Persistence | none | versioned Store with records, conflicts, meta |
| Second calendar | – | export loop (ExportManager) |
| Conflicts | – | decision table, policies, sensor, notification, services |
| Labels/attendees | ignored | parsed and exposed (issue #6) |
| Diagnostics | – | redacted dump |
| Branding | – |
brand/icon@2x, logo@2x, dark variants |
| Minimum HA | 2025.12 | 2026.9 |
| Verification | manual | 41 tests / 224 checks in tests/
|
| Attribute | Scenario | How it is met |
|---|---|---|
| Correctness of schedules | a daily recurring event with one EXDATE is queried for a month |
rruleset expansion, EXDATE respected (tested) |
| Correctness of day boundaries | an all-day event on 5 Oct is created in HA and read back from TimeTree |
-1 day on write, +1 day on read (round-trip tested) |
| Availability | TimeTree returns 401 mid-run |
one transparent re-login, then UpdateFailed
|
| Data safety | both TimeTree and Google changed | conflict queued, nothing written (tested) |
| Recoverability | TimeTree is unreachable for hours | last known snapshot stays; sensor shows stale Last updated
|
| Performance | a calendar with 500 events and 50 recurring series | executor jobs, bounded expansion, single fetch pass per refresh |
| Supportability | a user reports "nothing syncs" | diagnostics (transport, counters, records, conflicts) + debug logging |
| Extensibility | a user wants a second target calendar | change the target entity in the options; a per-target mapping is stored |
| ID | Risk | Impact | Mitigation |
|---|---|---|---|
| R-1 |
curl_cffi's impersonation profile ages out; TimeTree tightens its WAF |
writes fail with 422
|
profile is a single constant, diagnostics show the active transport, README documents the symptom; reads are unaffected |
| R-2 | The update/delete routes (PUT/DELETE /calendar/{id}/event/{uuid}) were inferred from the create route and are not confirmed against a live account |
update/delete could fail | plural fallback is attempted automatically, error is surfaced as HomeAssistantError; verification item on the release checklist |
| R-3 | TimeTree changes the payload requirements again | writes fail | payload mirrors the captured web-client payload (label_id, attendees, attachment, recurrences, alerts); all fields are in one builder |
| R-4 | Internal API change of the read shape | import breaks | parsing is defensive (missing keys default), labels/comments are optional |
| R-5 | Marker lost (target rewrites descriptions) | duplicate copies instead of updates | duplicates are reported as duplicate exported copy; ignore_forever can retire an event |
| R-6 |
newest_wins cannot compare target timestamps |
policy may pick the wrong side | it only auto-resolves single-sided changes and still asks when both changed (ADR 7) |
| R-7 | Export loop + HA restart timing | a missed run | the loop schedules a run 45 s after setup and every interval thereafter |
| R-8 | Store growth for large calendars | slow saves | debounced writes, records dropped when both sides deleted the event |
| Extension | Touch points |
|---|---|
| Another export target type | nothing to change — any calendar.* entity works |
| Per-calendar export settings |
options.py (per CalendarOption), export.py (target resolution) |
| Import unmanaged target events (already flagged, default off) |
export.py (_async_import_unmanaged), decide() TARGET_ONLY
|
| Single-occurrence edits / deletes |
calendar.py guard + TimeTree API research (TimeTree currently exposes series only) |
| Comments/images as attachments |
api.py activity endpoints, models.py (TimeTreeEvent.comments) |
Public calendars (/api/v2/public_calendars/...) |
new read path in api.py, no auth |
| Additional conflict resolutions (merge fields) |
conflicts.resolution_to_action, export._async_apply
|
| HACS default repository |
hacs.json, README, brand assets — already in place |
| Reference | Link |
|---|---|
| Software Design Document | Software-Design-Document |
| Workflow diagrams (GitDiagram + authored) | Workflow-Diagrams |
| Archived 1.1.3 design + defect list | Archive-1.1.3-Design-Documentation |
| Issue #2 (rrule), #3 (create kwargs), #5 (write/422/all-day), #6 (labels), #7 (recurrence PR) | https://github.com/acdcnow/Timetree-Import-for-Home-Assistant/issues |
| TimeTree-Exporter (API reference implementation) | https://github.com/eoleedi/TimeTree-Exporter |
| Home Assistant developer documentation | https://developers.home-assistant.io/ |
| Home Assistant 2026.9 device registry deprecations | https://developers.home-assistant.io/docs/device_registry_index/ |
🟢 Current — 2.0.x
(branch ha2026_09_dev, HA 2026.9+)
🗄️ Archived — 1.1.3
(branch main)
🔗 Links