Tip
Push notifications, calendars, and dashboards from the eduVulcan school e-journal.
Syncs the eduVulcan web API to a local SQLite database and diffs each run against the last one. Any changes go out to the terminal, macOS Calendar, MQTT, and an HTTP/iCalendar API, so Home Assistant (or anything else) can react to school events as they happen.
Tracks grades, attendance, exams, homework, messages (with an optional sender whitelist), lesson substitutions/cancellations, days off, absence excuses, remarks, and announcements. It handles multiple students under one parent account, and alerts when the undocumented eduVulcan API changes shape.
Exposes terminal output, macOS Calendar events, MQTT events with a persistent outbox, a JSON API on :8585 (including a feed of everything that changed), a per-student .ics feed, optional AI digests via any OpenAI-compatible API, and a read-only Agent Skill for asking questions in Claude Code, Codex, or Pi.
%%{init: {'theme':'base','flowchart':{'curve':'basis','nodeSpacing':40,'rankSpacing':70,'htmlLabels':true}}}%%
flowchart LR
accTitle: vulcan-notify data flow
accDescr: A sync loop pulls data from the eduVulcan web API using saved cookies, renewed by a Playwright login. Changes are upserted into SQLite and queued in an MQTT outbox. Mosquitto and the HTTP API feed Home Assistant, which pushes notifications to the parent's phone. The iCalendar feed and optional macOS Calendar sync land events in iCloud Calendar.
Vulcan([eduVulcan<br/>web API])
Auth[[Playwright login]]
subgraph Daemon[vulcan-notify]
direction TB
Loop{{sync loop}}
Diff{new or<br/>changed?}
DB[(SQLite)]
Outbox[(MQTT outbox)]
API(HTTP API :8585)
end
subgraph Consumers[Home Assistant and calendars]
direction TB
Mosq[(Mosquitto)]
HA{{Home Assistant}}
Cal[(iCloud Calendar)]
end
Phone([Parent phone])
Auth -.->|cookies| Loop
Vulcan -->|HTTPS · JSON| Loop
Loop --> Diff
Diff -->|upsert| DB
Diff -->|enqueue| Outbox
DB --> API
Outbox -->|school/#| Mosq
Mosq --> HA
API -->|REST sensors| HA
API -->|.ics feed| Cal
Diff -.->|AppleScript<br/>macOS only| Cal
HA -->|push| Phone
Cal --> Phone
classDef storage fill:#ecfdf5,stroke:#16a34a,color:#064e3b;
classDef external fill:#fef3c7,stroke:#d97706,color:#78350f;
classDef actor fill:#eef2ff,stroke:#4f46e5,color:#312e81;
class DB,Outbox,Mosq,Cal storage;
class Vulcan,Auth external;
class Phone actor;
style Daemon fill:#f8fafc,stroke:#94a3b8
style Consumers fill:#f8fafc,stroke:#94a3b8
linkStyle default stroke:#64748b,stroke-width:1.5px
linkStyle 1,2,6,7,8,9 stroke:#2563eb,stroke-width:2px
linkStyle 3,4,5 stroke:#16a34a,stroke-width:2px
linkStyle 11,12 stroke:#ea580c,stroke-width:2px
- Auth - log in once in a Playwright browser to save session cookies. If credentials are stored, the tool renews expired sessions headlessly.
- Fetch - call the eduVulcan web API with those cookies: grades (all periods), attendance (last 90 days), exams, homework, messages, the schedule, days off, absence excuses, remarks, and announcements.
- Diff and persist - compare against SQLite and upsert (
ON CONFLICT ... DO UPDATE). Exams and homework that drop out of the API are soft-deleted. The first sync stores a baseline and reports nothing. Every change a later run reports is also kept in a change log, which backs/api/changes. - Watch for drift - compare each response's structure (key paths and types, never values) with what was learned before. A renamed or missing field, a type change, or a section that suddenly returns nothing raises an alert.
- Publish - print changes, write them to macOS Calendar, and drain the MQTT outbox to the broker. The outbox survives broker outages.
- Serve - a separate long-running process (
python -m vulcan_notify.api) serves HTTP and iCalendar from the same SQLite file.
Module breakdown, schema, MQTT topic map, and endpoint reference: docs/architecture.md.
Requires Python 3.12+ and uv.
git clone https://github.com/kintecus/vulcan-notify.git
cd vulcan-notify
uv sync
uv run playwright install chromium
cp .env.example .env # optional, see Configuration
uv run vulcan-notify auth # opens a browser
uv run vulcan-notify sync| Command | Description |
|---|---|
vulcan-notify auth |
Browser login, saves session cookies |
vulcan-notify test |
Check whether the saved session is valid |
vulcan-notify sync |
Fetch, diff, and publish changes (default) |
vulcan-notify heartbeat |
Publish the retained MQTT heartbeat only, no sync |
vulcan-notify calendar |
Force a clean re-sync of macOS Calendar events |
vulcan-notify tui |
Textual browser for synced content (uv sync --extra tui) |
vulcan-notify summarize [--type sync|messages] [--days N] |
AI digest of recent changes or messages (needs LLM_API_KEY) |
vulcan-notify shapes |
Print the learned API response structure as JSON (no values) |
In production, vulcan-notify runs in Docker on a Proxmox LXC as two services: the sync loop and the API. It publishes to the Mosquitto add-on in Home Assistant OS.
- MQTT (event-driven) - topics follow
<prefix>/<student>/<segment>/<change_type>, e.g.school/alice/grades/neworschool/alice/remarks/new, with JSON payloads. Days off and announcements that all students share go toschool/all/...once. API drift alerts go toschool/system/api/drift. A retainedschool/statusheartbeat with an LWT lets HA detect a silent daemon. - HTTP (pull-based) - REST sensors poll
/api/grades/monthly,/api/messages,/api/schedule, and others./api/healthreturns 503 when data goes stale (quiet hours don't count toward staleness) and lists recent API drift. Any calendar client can subscribe to/calendar/<name>.ics.
Setup: docs/deployment.md.
skills/school-brief is an Agent Skill (Claude Code, Codex, Pi) that turns a coding agent into a read-only school chief of staff: morning briefs, "anything to act on before Friday", and draft replies to teachers. Its vulcan-ask client only reads the HTTP API, so the agent never logs in, syncs, or sends anything:
export VULCAN_API_URL=http://<api-host>:8585
skills/school-brief/scripts/vulcan-ask brief # everything a morning brief needs, one call
skills/school-brief/scripts/vulcan-ask changes --since 7d # what changed: grades, exams, messages, ...
skills/school-brief/scripts/vulcan-ask attendance --days 30 # absences, with excuse statusTwo endpoints exist mainly for it. /api/changes?since=7d returns the change log, and /api/attendance lists non-present lessons. Setup, including the Claude Code deny rules that keep the agent read-only, is in the skill's README.
eduVulcan sessions expire after a few hours. For unattended runs, store your credentials and sync will re-authenticate headlessly. The macOS Keychain keeps them off disk:
security add-generic-password -s vulcan-notify -a your.email@example.com -wOr set VULCAN_LOGIN and VULCAN_PASSWORD in .env.
Map each student to an existing macOS calendar:
CALENDAR_MAP={"Alice Smith": "School Alice", "Bob Johnson": "School Bob"}
sync then writes exams and homework as all-day events with an alarm, and iCloud syncs them to your other devices. Event UIDs are stored in the database for deduplication. Soft-deleted items also lose their events.
Set these as environment variables or in .env.
| Variable | Default | Description |
|---|---|---|
DB_PATH |
vulcan_notify.db |
SQLite database path |
SESSION_FILE |
session.json |
Saved session cookies |
VULCAN_LOGIN / VULCAN_PASSWORD |
(none) | Credentials for headless re-auth |
SYNC_ATTENDANCE_DAYS |
90 |
Attendance lookback window |
SYNC_MESSAGE_BACKFILL_BATCH |
10 |
Legacy message bodies to refetch per run |
SYNC_MESSAGE_HISTORY_PAGES |
3 |
Older inbox pages (50 each) to backfill per run, until the oldest is stored |
SYNC_HISTORY_KEEP_DAYS |
90 |
Retention for sync run history and the change log |
POLL_INTERVAL |
1800 |
Seconds between syncs in the service loop |
QUIET_HOURS_START / QUIET_HOURS_END |
0 / 5 |
Hours (0-23) when syncing pauses |
QUIET_HOURS_TZ |
Europe/Warsaw |
Time zone for the quiet window (the container stays on UTC) |
STALE_AFTER_SECONDS |
3600 |
Data age at which /api/health reports stale |
MESSAGE_SENDER_WHITELIST |
(empty) | Comma-separated sender names, empty shows all |
DISPLAY_NAME_MAP |
(empty) | JSON map of full name to nickname for notification titles |
CALENDAR_MAP |
(empty) | JSON map of student name to macOS calendar |
CALENDAR_REMINDER_HOURS |
24 |
Alarm lead time |
MQTT_ENABLED |
false |
Enable MQTT publishing |
MQTT_BROKER / MQTT_PORT |
localhost / 1883 |
Broker address |
MQTT_USERNAME / MQTT_PASSWORD |
(none) | Broker auth |
MQTT_TOPIC_PREFIX |
school |
Topic root |
MQTT_STATUS_SUFFIX |
status |
Retained heartbeat topic under the prefix |
LLM_BASE_URL |
https://api.cerebras.ai/v1 |
OpenAI-compatible endpoint for summaries |
LLM_API_KEY |
(none) | Enables summaries |
LLM_MODEL |
gpt-oss-120b |
Summary model |
PROMPTS_FILE |
prompts.toml |
Summary prompt templates |
NTFY_TOPIC / NTFY_SERVER |
vulcan-notify / https://ntfy.sh |
ntfy target for deploy notifications |
LOG_LEVEL |
INFO |
Logging level |
docs/architecture.md- pipeline, schema, MQTT payloads, endpointsdocs/deployment.md- Docker, Proxmox LXC, and systemddocs/eduvulcan-api.md- reverse-engineered eduVulcan web API referenceskills/school-brief/README.md- agent skill setup and permission rules