Annual Events is a local-first Home Assistant custom integration for birthdays, anniversaries, pet birthdays, memorials, work anniversaries, holidays, name days, and custom yearly dates.
It keeps one collection-level config entry. Each annual event is an internal record—not a helper or another config entry—and can optionally be projected as its own sensor. Rich results travel through WebSocket commands, action responses, the calendar entity, and supported LLM tools rather than being squeezed into entity state strings.
- UI-only, credential-free setup with one config entry
- Responsive sidebar management page for list, add, edit, delete, search, sort, filter, and quick toggles
- Optional original year with correct age/anniversary numbers
- Configurable 29 February handling
- Versioned local storage and concurrency-safe mutations
- Deterministic name, alias, category, and notes search
- Aggregate next-event, next-important-event, and upcoming-count sensors
- Optional stable-ID sensor for each enabled event
- All-day calendar with expanded occurrences across years
- Structured query and mutation actions
- Authenticated WebSocket API with administrator-only mutations
- Read-only LLM tools on Home Assistant versions that support contributed integration tools
- Privacy-redacted diagnostics; no telemetry or network requests
Annual Events requires Home Assistant 2026.7 or newer. Contributed read-only LLM tools require Home Assistant 2026.8 or newer; the rest of the integration remains available on 2026.7.
Screenshots have not yet been captured for this first release. The checked-in panel is complete and build-free.
- Open HACS.
- Open the menu and choose Custom repositories.
- Add
https://github.com/conorod1992/annual-eventsas an Integration. - Install Annual Events and restart Home Assistant.
No separate Lovelace resource or frontend build is required.
Copy custom_components/annual_events into the custom_components directory under your Home Assistant configuration directory, then restart Home Assistant.
Go to Settings → Devices & services → Add integration, search for Annual Events, and confirm setup. Only one Annual Events collection can be configured.
Open Annual Events in the sidebar. The page supports desktop and mobile layouts, keyboard-usable controls, loading/error/empty states, search, category and status filters, important-only filtering, sorting by name or next occurrence, and confirmation before deletion. Non-administrators can read the collection; the backend permits create, update, and delete operations only to administrators through the panel API.
Use the integration's Configure button to choose:
- the leap-day policy;
- the period used by the upcoming count sensor;
- whether the sidebar panel is shown.
Individual events are always managed from the dedicated panel or actions, never from the options flow.
Month and day are required. The original year is optional and is stored as a real optional component—Annual Events never invents a placeholder year.
When the year is absent, next occurrence and days remaining still work, while age/anniversary number is omitted. When it is present, the original date is occurrence zero: a birth or wedding on 7 August 2000 has occurrence 0 in 2000 and occurrence 26 in 2026.
Calculations use Home Assistant's configured local timezone. Range queries are inclusive at both ends, can cross New Year, and can return the same record once per covered year.
For an event recorded on 29 February, choose one collection-wide policy:
- observe it on 28 February in non-leap years (default);
- observe it on 1 March in non-leap years;
- return it only in leap years.
The selected policy is shared by the panel, sensors, calendar, actions, WebSocket queries, and LLM tools.
The integration creates:
sensor.next_annual_event: ISO date state plus bounded metadata for the next enabled event;sensor.next_important_annual_event: the same projection for important enabled events;sensor.upcoming_annual_events: numeric count in the configured period;calendar.annual_events: enabled records as concrete all-day occurrences.
Turning on Expose individual sensor creates a date sensor with a unique ID based on the immutable event ID. Renaming an event does not create a new entity. Turning exposure off removes it from runtime while retaining its entity-registry identity for a future re-enable; deleting the record removes the orphaned registry entry.
No sensor contains an unbounded event list.
Available actions are:
annual_events.create_eventannual_events.update_eventannual_events.delete_eventannual_events.searchannual_events.get_upcomingannual_events.get_between
Update and delete require the exact stable event ID. Query actions always return structured response data. Mutation actions optionally return the affected record when the caller requests a response.
Create an event in the automation UI's Edit in YAML editor:
action: annual_events.create_event
data:
name: John's birthday
month: 8
day: 7
category: birthday
aliases:
- John
important: true
enabled: true
expose_entity: true
response_variable: created_eventQuery with a response variable:
action: annual_events.get_between
data:
start: "2026-12-01"
end: "2027-01-10"
important_only: true
limit: 100
response_variable: annual_events_resultThe response has this shape:
count: 2
occurrences:
- event_id: 4df57b76-1f7b-4b6c-80cb-04abb8b8a719
name: Example birthday
category: birthday
occurrence_date: "2026-12-14"
occurrence_number: 30
important: true
days_until: 42Notify seven days before the next important event:
alias: Important annual event in seven days
triggers:
- trigger: time
at: "09:00:00"
conditions:
- condition: template
value_template: >-
{{ state_attr('sensor.next_important_annual_event', 'days_until') == 7 }}
actions:
- action: notify.notify
data:
title: Annual event reminder
message: >-
{{ state_attr('sensor.next_important_annual_event', 'name') }} is in seven days.Notify on the morning of a birthday exposed as an individual sensor (replace the entity ID):
alias: Birthday this morning
triggers:
- trigger: time
at: "08:00:00"
conditions:
- condition: template
value_template: "{{ states('sensor.mums_birthday') == now().date().isoformat() }}"
actions:
- action: notify.notify
data:
message: "Mum's birthday is today."Query and announce the next event (replace the TTS target entities):
sequence:
- action: annual_events.get_upcoming
data:
days: 366
limit: 1
response_variable: next_events
- action: tts.speak
target:
entity_id: tts.home_assistant_cloud
data:
media_player_entity_id: media_player.kitchen
message: >-
{% set event = next_events.occurrences[0] %}
The next annual event is {{ event.name }} on {{ event.occurrence_date }}.On Home Assistant releases supporting contributed llm.py tools, Annual Events supplies three read-only tools:
search_annual_eventsget_upcoming_annual_eventsget_annual_events_between
They support questions such as “When is Mum's birthday?”, “How old will Mum be on her next birthday?”, and “What important events occur between 1 December and 10 January?” Names and aliases are searched deterministically. A missing original year returns no occurrence number rather than a guessed age.
Write tools are deliberately not exposed in this release. Creating, changing, or deleting data through a model introduces permission and ambiguity risks; use the administrator-protected panel or exact-ID actions instead. Non-LLM Assist custom intents are not included.
Annual Events makes no network requests and contains no analytics or telemetry. Records are stored locally using Home Assistant's versioned storage under .storage/annual_events.events. Normal logs never include whole personal records, and diagnostics contain counts, categories, schema/version information, and options only—not names, aliases, notes, exact dates, or original years.
Home Assistant's normal backups include .storage. Take a backup before large imports or upgrades and do not edit storage files while Home Assistant is running. If storage cannot be loaded, setup fails without replacing it with an empty collection.
The frontend uses authenticated commands under annual_events/: list, get, create, update, delete, search, upcoming, between, and settings. Read commands accept bounded filters and limits. Mutation commands enforce administrator status in Python, regardless of what controls the browser displays. between is capped at 3,660 days and 5,000 results.
- Panel missing: confirm Show Annual Events in the sidebar is enabled, reload the integration, and hard-refresh the browser.
- Individual sensor missing: both Enabled and Expose individual sensor must be on for that event.
- Age is absent: add the real original year. This is intentional when it is unknown.
- Unexpected leap-day date: review the integration's leap-day option.
- Action says unknown ID: retrieve the stable ID from the panel, search response, sensor attributes, or
annual_events.search; names are never accepted for deletion. - Setup fails after storage damage: restore
.storage/annual_events.eventsfrom a Home Assistant backup. The integration will not silently overwrite unreadable data.
Use Python 3.14 for the current Home Assistant development stack:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install ".[dev]"
ruff format --check .
ruff check .
mypy custom_components/annual_events
pytest --cov=custom_components.annual_eventsThe frontend is a checked-in, dependency-free web component; no Node.js build is needed. CI also runs hassfest and HACS repository validation.
- English is the only included translation.
- LLM access is read-only and depends on the Home Assistant contributed-tool platform.
- JSON/CSV/ICS/vCard import and export are not included yet.
- Country holiday sources and external holiday APIs are intentionally absent.
- Screenshots are not yet available.
Planned work includes versioned JSON import/export with validation and duplicate policies, additional translations, bulk editing, optional import formats, and safe write tools if Home Assistant exposes a sufficiently clear permission and confirmation model.
MIT. See LICENSE.