Home Assistant custom integration for menstrual cycle and contraception tracking, with configurable notifications for "supporters" - anyone the administrator chooses to keep informed (partner, roommate, family, in any number and any combination, no assumption of a monogamous couple).
Status: early alpha (v0.x). Not feature-complete. See
CHANGELOG.mdfor what's actually implemented in each release, andANALYZA-A-ROADMAP.mdfor the full plan.
This is a home-automation tool, not a medical device. Calendar-based fertility predictions are not a reliable contraception method on their own. Always consult a healthcare professional.
Three roles, deliberately not tied to any particular relationship shape:
- Cycle owner: the person whose cycle/contraception is being tracked. One config entry ("integration instance") per cycle owner - track as many people in one Home Assistant as you want, each fully independent. The cycle owner always sees everything about themselves on their own dashboard.
- Supporter: anyone the administrator wants kept informed about a cycle owner - partner, roommate, family, whoever. A supporter is notified only about the categories (PMS window, upcoming period, contraception restock, missed dose, fertility window) and at the detail level (general vs. detailed) they've been subscribed to, and only for the cycle owner(s) they're subscribed under. The same person can be a supporter for several different cycle owners at once (e.g. polyamory), and each of those subscriptions is configured independently - there's no assumption of a single monogamous pair. A subscription controls notifications, not dashboard access - see "What a subscription does and does not do" below, and read it before treating this as a privacy boundary.
- Administrator: whoever has admin rights on the Home Assistant instance - which is also whoever can reach Settings > Devices & Services in the first place, so this isn't an extra restriction, just how the platform already works. The administrator decides which supporters exist and what they're subscribed to, via Options Flow ("Configure" on the integration) or
perioder.update_settings. There's no separate cycle-owner-side consent step in this project's model - seeANALYZA-A-ROADMAP.mdsection 2.5 for the reasoning.
One practical consequence: the PMS-window binary sensor always exists (so an admin can verify it in Developer Tools, or wire it into an automation), but it's deliberately left off the example owner-facing dashboard card - PMS visibility is a supporter-notification thing, not something the cycle owner needs surfaced about themselves.
Enforced by the integration. Which supporter's device receives which
notification, and how much detail it carries, is decided in code from the
per-pair subscription (notifications.py). A supporter who is not subscribed
to a category never receives a push about it. That part is real.
Not enforced by the integration. Everything Perioder publishes is
ordinary Home Assistant entities - sensor.*, binary_sensor.*,
calendar.*. Home Assistant has no per-entity access control for
non-administrator users: any logged-in user who can open a dashboard
containing an entity, or who opens the search dialog, can read its state. The
split between dashboard_test.yaml and dashboard_test_admin.yaml is a
convention about what you put in front of whom, not a permission boundary,
and a supporter with a Home Assistant login can see more than their
subscription lists.
If you need a stronger separation, the mechanisms are Home Assistant's own, not this integration's: give each person their own dashboard, mark views admin-only where appropriate, and remember that a shared login shares everything. Do not set this up for someone on the understanding that their data is compartmentalised by Perioder - it is compartmentalised by who you give a login to.
- HACS > Integrations > the three-dot menu > Custom repositories.
- Add
https://github.com/Michailjovic/Perioder, category Integration. - Install Perioder and restart Home Assistant.
- Settings > Devices & Services > Add Integration > Perioder.
- During setup, name it exactly Test (this becomes the device name and
determines entity IDs - the ready-made dashboard below assumes this name).
If you want to test the daily reminder, pick a Your own notify device
too (any
mobile_appdevice) - without one, the reminder/escalation has nowhere to send to and silently does nothing (a debug log line only). - Install the card-mod frontend module first (HACS > Frontend > search
"card-mod" > Install > reload the browser, Ctrl+Shift+R) -
dashboard_test.yaml's calendar cards are the built-intype: calendarcard withinitial_view: dayGridMonthplus acard_modstyle settingmin-heightonha-card, which fixes the month-grid row height clipping a plain built-in card has (found live, v0.9.1/v0.9.2 - a third-partyatomic-calendar-revivecard was tried as a workaround in v0.9.3, but thiscard_mod+initial_viewcombination turned out to work fine, confirmed live 2026-08-07). It's the one non-native piece of this dashboard; every control/sensor card is still a plain entity the integration provides itself, no helpers or scripts. - Two separate single-view dashboards (v0.9.12) instead of one dashboard
with tabs:
dashboard_test.yaml(day-to-day: confirm pill, log period, cycle day/next period, symptoms, a colored period/fertile/pause calendar- no PMS) and
dashboard_test_admin.yaml(everything else: PMS window, supporters, pack start date correction, test notification, pause switch, the same calendar plus PMS, the detailed pill-log calendar, and the shared calendar). Each file's card list is onevertical-stack, so it renders as a single predictable column regardless of screen width - a plain flat card list would otherwise get auto-split into multiple columns by Lovelace's masonry view on wide screens. Add each the same way: Settings > Dashboards > Add Dashboard > "New dashboard from scratch" > open it > pencil icon (Edit Dashboard) > three-dot menu > "Edit in YAML"
delete what's there > paste the file's contents > Save. Do this twice, once per file. (
lovelace_example.yamlis a single small entities card, for dropping into a dashboard you already have - no calendar, no card-mod needed.) - no PMS) and
- From the dashboard: click the date entity and pick a date (today, or
earlier if you're only entering it the next day) - that logs the period
start immediately, no separate confirm step. The PMS dropdown forces the
PMS window on/off/auto for testing without waiting for the real date.
Once the period is over, optionally set
date.*_last_period_end(the real last day of bleeding, inclusive) - the calendar's period block for that cycle then shows the real span instead of theperiod_durationestimate. It resets itself the next time you log a new period start. - To test contraception: just press
button.*_confirm_pill_taken- the first confirmation auto-activates tracking with that day as the pack start date (v0.9.7). After that,sensor.*_contraception_statusshowspending/taken/missed/paused/inactivefor today. If the real first day was earlier than today, setdate.*_pack_start_dateinstead (v0.9.9) - same idea asdate.*_last_period_start: pick a date, that is the action, backdating included. Either way it's a one-time thing -day_in_pack()wraps to the next pack automatically from then on, pause days/reminders/restock timing all compute themselves, nothing needs pressing again each cycle.perioder.start_new_pack(Developer Tools > Actions) does the same as the date entity, for automations/voice/NFC. - The day-to-day calendar combines three separate entities - predicted
periods, fertile windows, and pack-pauses - each getting its own color
(auto-assigned by the calendar card in entity order; the built-in card
has no way to pin a specific color to a specific one, that's a
long-standing open Home Assistant feature request). PMS is deliberately
left off it (that's admin-only, see below). The admin dashboard's
"Kalendář cyklu (přehled)" adds PMS as a fourth entity; its detailed
calendar (
cycle_calendar) additionally shows every logged pill confirmation as its own event - open one to see how many minutes early/late it was confirmed vs. the daily reminder time. - To test the reminder/escalation: use
perioder.update_settingsto setreminder_timea couple of minutes from now (and optionally lowerescalation_grace_minutes) - a check runs every 15 minutes, so give it at least one tick past your chosen time. Leave the dose unconfirmed to see the missed-dose notification and escalation; press the button to see it stop.switch.*_pause_notificationsmutes all of this without losing any data. - Settings > Devices & Services > Perioder > Configure lets you edit settings
and add supporters (notification target, categories, detail level) - only
reachable by an HA administrator, by design.
perioder.update_settingsdoes the same thing for settings from an automation/voice/NFC. - The symptom buttons log a timestamped entry each press;
sensor.*_last_symptomshows the most recent one.perioder.export_symptom_logreturns the full history as response data - acsvfield (flat text you can paste into a file) and arowsfield (structured, for templates). Call it from Developer Tools > Actions with "Return response data" ticked, or withresponse_variablein a script. Nothing is written to disk: before v0.11.0 this wrote into Home Assistant'swww/folder, which is served publicly at/local/with no authentication - see the changelog for that fix. - The shared calendar shows only generic "Citlivé období" blocks - which
block types it reflects at all is the
shared_calendar_categoriessetting (defaults to just periods). number.*_pills_in_stockis a real, settable count of tablets at home - set it after buying more (or viaperioder.set_pills_in_stock); each confirmed dose decrements it by one. Once it drops to or belowlow_stock_threshold(default 5), you and subscribed supporters get warned once - separate from the pack-days-remaining warning above.- On a phone with the Home Assistant Companion app, the reminder and
escalation notifications now carry two buttons: "Vzal(a) jsem" confirms
today's dose without opening the app; "Odložit" postpones the nag by
escalation_repeat_minuteswithout marking anything taken or missed. sensor.*_fertilityreportssuppressedwhile contraception is active and no dose has been recorded missed in the last full pack cycle, instead of a fertile/low/safer estimate. Combined hormonal contraception taken as prescribed suppresses ovulation, so there is no fertile window to report - and while contraception is on, the cycle length is derived from the pack rhythm, which made the old estimate a number with no referent. The estimate returns the moment a dose is missed or contraception is switched off; the raw value stays visible in thecalendar_estimateattribute throughout. The fertile calendar blocks and the "fertile window starting" supporter notification follow the same rule. This is not a statement about contraceptive efficacy - it only decides whether a calendar estimate is worth publishing.- When something looks wrong, Settings > Devices & Services > Perioder > the device > Download diagnostics gives one JSON file with the stored inputs beside what the engine concludes from them - effective vs. configured settings, the pill log, every notification dedup key, and the live pack state with its blockers and drift assessment. Names are redacted and the symptom log is summarised, but it is still health- adjacent data: read it before attaching it to a public issue.
Prefer Developer Tools > Actions, or calling this from an automation/voice
command/NFC tag? perioder.log_period_start, perioder.log_period_end,
perioder.set_pms_override, perioder.log_pill_taken, perioder.start_new_pack,
perioder.set_contraception_active, perioder.update_settings,
perioder.pause_notifications, perioder.log_symptom,
perioder.export_symptom_log, and perioder.set_pills_in_stock all exist
and do exactly the same thing as the matching entities/Options Flow.
- Config + options flow (settings, supporter management), plus
perioder.update_settingsfor changing settings outside the flow - covers every setting Options Flow does. - Settings/supporters live in the config entry; runtime state (cycle, contraception, symptoms, notification bookkeeping) lives in a separate per-entry Store.
- Sensors: cycle day, phase, fertility, next period, contraception status
(
inactive/paused/pending/taken/missed), pack days remaining, last symptom logged, supporters overview (count + per-supporter attributes, for a dashboard markdown card). - Binary sensors: period active, PMS window (with manual override), contraception tracking active, pill taken today.
- Date entities: log/backdate the period start directly from the UI, plus an
optional real period end (used to show the actual span in the calendar
instead of the
period_durationestimate for that cycle). - Select entity: PMS override (auto / active / inactive).
- Button entities: confirm today's pill taken, log each of the 4 built-in symptoms (cramps, headache, low energy, mood change).
- Switch entity: pause all notifications for this cycle owner.
- Number entity:
pills_in_stock- settable physical tablet count, auto-decremented per confirmed dose. - Calendar entities:
cycle_calendar(detailed - predicted period/fertile/ pms/pack-pause blocks plus every logged pill confirmation, with the delay vs. the reminder time),period_calendar/fertile_calendar/pms_calendar/pause_calendar(the same predicted blocks split one kind per entity, so a Lovelace calendar card can color each one differently -pms_calendaris meant for the admin dashboard only), andshared_calendar(generic "sensitive period" blocks with no detail, for exporting to a shared family calendar - which block types show up at all is configurable). - Notifications: daily contraception reminder + escalation to your own
device (with actionable "Vzal(a) jsem"/"Odložit" buttons, v0.8.0), a
missed-dose alert to subscribed supporters (with a fertile-window
heads-up folded in), a one-shot "pack running low" notice (current pack's
active days ending soon) and a separate one-shot "low stock" notice
(real
pills_in_stockcount dropping to/belowlow_stock_threshold) to supporters subscribed to restock alerts. - Services:
log_period_start,log_period_end,set_pms_override,log_pill_taken,start_new_pack,set_contraception_active,update_settings,pause_notifications,log_symptom,export_symptom_log,set_pills_in_stock(same effect as the entities/Options Flow above, for automations/voice/NFC).
Not yet implemented: pms/period/fertility as their own
transition-triggered supporter notifications (e.g. "PMS window just
started"), and history/trend graph cards (the sensors have the needed
data, no graph card is wired into dashboard_test.yaml yet). The
notification dispatch code (including the v0.8.0 actionable buttons)
also hasn't been exercised against a live Home Assistant instance yet -
only its decision logic has been verified standalone; please report if a
notification doesn't arrive, or if the "Vzal(a) jsem"/"Odložit" buttons
don't show up on your phone. See CHANGELOG.md and ANALYZA-A-ROADMAP.md.
Added after a live incident (2026-08-31) where pack_start_date drifted
six days from reality and the notification engine went silent for a week
with nothing able to say so - see perioder-pack-drift-incident.md (project
memory) and ANALYZA-A-ROADMAP.md's M10 for the full writeup. New surface:
- Entities:
sensor.*_attention(open items needing your attention, on the main device card - not diagnostic),number.*_pill_number_today("which tablet today" - re-anchorspack_start_dateby counting, not by remembering a date),date.*_log_pill_for_date(retroactive single-day confirmation),button.*_undo_pill_today,button.*_bought_pack,switch.*_skip_current_pack(this pack cycle only, self-clearing),switch.*_contraception_active. - Conversation, not silence: once a pack overruns without a confirmed
period, or a new pack is due, the integration asks directly instead of
guessing or staying quiet -
[Začala dnes]/[Ještě ne]and[Začala jsem]/[Ještě mám periodu]/[Nemám balení]. A confirmed "Začala jsem" every cycle is what makes the original drift structurally unrepeatable, not just detectable. - Catch-up prompt when unconfirmed pill days pile up, a drift
notification (
[Posunout]/[Je to správně]) when the pack model and a confirmed period start disagree, and a watchdog for total silence (including a pause that has run implausibly long). - Admin repair services:
perioder.set_pill_log_range,perioder.export_pill_log,perioder.undo_pill_taken- for straightening out historical data, not day-to-day use. period_durationis now learned from volunteered period start/end pairs once enough samples exist, instead of staying fixed at the configured setting forever.
Not yet verified live against a real HA instance - see the "Not yet implemented"/testing caveats below, which apply here too.
Lighting scene during period/PMS, adding to a shopping list when the
contraception pack runs low or a period is coming up, and a heating pad
reminder - none of these are part of the integration itself, so nothing
installs automatically. See BLUEPRINTS.md for import links and details.
cycle_math.py and pill_math.py are plain Python with no Home Assistant
dependency by design, specifically so they can be unit tested without
installing Home Assistant itself:
pip install pytest
pytest tests/ -vtests/conftest.py loads just those two modules (plus const.py) directly
by file path instead of importing the integration package normally, since
the normal import path runs custom_components/perioder/__init__.py, which
does need Home Assistant. Everything else (config flow, entities,
services, the notification engine) isn't covered by this test suite yet -
it's been checked via standalone logic simulations during development (see
CHANGELOG.md) and manual testing against a real Home Assistant instance,
not automated tests.
- No dashboard screenshots in this README yet - the author hasn't captured any from a running instance.
- The v0.8.0 actionable notification buttons ("Vzal(a) jsem"/"Odložit") depend on the Home Assistant Companion app version on the phone - not yet confirmed to actually render as tappable buttons on a real device.
- Name your cycle owner something that won't collide with another
integration's entities (found live 2026-08-08). If you also run the
cyclist integration (the project
this one's fertility/phase math was originally adapted from, see
cycle_math.py) for the same person, both integrations end up wanting entity IDs likesensor.<name>_cycle_day- whichever loads second gets silently suffixed_2by Home Assistant's entity registry, and it's very easy to end up with a dashboard pointed at the other integration's entity instead of Perioder's (same-shaped state, completely different - and in this case wildly wrong-looking - numbers, no error anywhere to flag it). If you hit numbers that don't match the logged dates, check Developer Tools > States for a same-named_2entity before assuming a math bug. Cleanest fix: don't run both integrations under the same name for the same person, or rename one so their entity ID prefixes never overlap. - See "Not yet implemented" above and
ANALYZA-A-ROADMAP.mdfor the rest.
Full analysis, control model, and roadmap: ANALYZA-A-ROADMAP.md.
Optional automation blueprints: BLUEPRINTS.md.