A Python-based Telegram bot that monitors WebUntis for timetable changes and sends AI-generated notifications when updates are detected.
- Fetches timetable data from WebUntis via JSON-RPC API, with optional REST API token support
- Detects changes in lessons, rooms, teachers, or cancellations
- AI-powered summaries using GitHub Models (GPT-5)
- Automatic Telegram notifications
- Stateful watcher with persistent
state.jsonstorage to avoid duplicate notifications across restarts - Continuous monitoring with configurable polling interval
- Secure error handling: credentials and tokens are automatically scrubbed from all log output
- Optional system tray integration on Windows; automatically falls back to headless mode when unavailable
- Python 3.9+
- A WebUntis account
- A Telegram bot token and your chat ID
- A GitHub personal access token (for GitHub Models AI)
Clone the repository:
git clone https://github.com/aoyn1xw/Untis-watcher.git
cd Untis-watcherCreate and activate a virtual environment:
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # macOS/LinuxInstall dependencies:
pip install -r requirements.txtNote: On Windows, the bot can use pystray to run in the system tray, which requires Pillow. If tray dependencies are unavailable (for example on headless Linux/VPS), it runs directly in terminal mode.
- Open Telegram and search for
@BotFather - Send
/newbotand follow the instructions - Save the bot token (looks like
123456789:ABCdefGHIjklMNOpqrsTUVwxyz) - Start a chat with your new bot
The most reliable method is to use the Telegram API directly:
- Add your bot token to
.envfirst (see step 5) - Send any message to your bot in Telegram (e.g. "Hi")
- Run this in the project directory:
python -c "
import os, requests
for line in open('.env'):
line = line.strip()
if line and not line.startswith('#') and '=' in line:
k, v = line.split('=', 1)
os.environ[k.strip()] = v.strip()
token = os.getenv('TELEGRAM_TOKEN')
r = requests.get(f'https://api.telegram.org/bot{token}/getUpdates')
print(r.json())
"- Look for
'chat': {'id': 123456789, ...}in the output — that number is your chat ID.
- Go to GitHub Settings → Developer Settings → Personal Access Tokens
- Click "Generate new token" → "Generate new token (classic)"
- Give it a name like "Untis Watcher"
- Select the
models:readscope - Generate and save the token (looks like
github_pat_...)
- Log into WebUntis in your browser
- Open Developer Tools (F12)
- Go to the Network tab
- Navigate to your timetable in WebUntis
- Look for a request to
dataorgrid?timetableType=MY_TIMETABLE - Check the Request URL or look in the JWT token for
person_idorelementId - Save this number (e.g.,
616)
Create a .env file in the project root with the following content:
# WebUntis Configuration
UNTIS_SERVER="your-school.webuntis.com"
UNTIS_SCHOOL="your-school-slug"
UNTIS_USER="your.username"
UNTIS_PASSWORD="your_password"
UNTIS_ELEMENT_TYPE="5"
UNTIS_ELEMENT_ID="your_element_id"
# GitHub Models (AI)
GITHUB_TOKEN="your_github_token"
AI_MODEL="gpt-5"
# Telegram
TELEGRAM_TOKEN="your_telegram_bot_token"
TELEGRAM_CHAT_ID="your_telegram_chat_id"
# Optional Settings
POLL_INTERVAL="300"
DAYS_AHEAD="7"Example with placeholder values:
UNTIS_SERVER="example-school.webuntis.com"
UNTIS_SCHOOL="example-school"
UNTIS_USER="student.username"
UNTIS_PASSWORD="replace_me"
UNTIS_ELEMENT_TYPE="5"
UNTIS_ELEMENT_ID="123456"
GITHUB_TOKEN="github_pat_replace_me"
AI_MODEL="gpt-5"
TELEGRAM_TOKEN="123456789:replace_me"
TELEGRAM_CHAT_ID="123456789"
POLL_INTERVAL="300"
DAYS_AHEAD="7"Configuration Notes:
UNTIS_SERVER: Your WebUntis domain (from the URL)UNTIS_SCHOOL: The school slug (short name in the URL, not the full name)UNTIS_ELEMENT_TYPE: Usually5for student,1for classUNTIS_ELEMENT_ID: Your student/person ID from WebUntisPOLL_INTERVAL: Seconds between checks (300 = 5 minutes)DAYS_AHEAD: How many days of timetable to fetch
Run the bot:
python main.pyOr double-click main.py to run it directly.
The bot will:
- Start in the background with a system tray icon
- Load or create its persistent
state.jsonbaseline - Monitor your timetable every 5 minutes
- Show notifications only for real timetable changes
To quit: Right-click the system tray icon and select "Quit"
First run: The bot will save your current timetable to state.json as a baseline and won't send notifications until actual changes are detected.
Run in the foreground:
python main.pyOr use screen/tmux to run in background (see Deployment Options below).
If tray dependencies are available, the bot runs in the system tray when started. Look for the blue circle icon in your taskbar notification area.
To start automatically on boot:
- Press
Win + Rand typeshell:startup - Create a shortcut to your Python script:
- Right-click → New → Shortcut
- Location:
C:\Users\ayon1xw\Documents\GitHub\Untis-watcher\venv\Scripts\pythonw.exe "C:\Users\ayon1xw\Documents\GitHub\Untis-watcher\main.py" - Name it "Untis Watcher"
- The bot will now start silently when you log in
Alternative: Task Scheduler
- Open Task Scheduler (
Win + R, typetaskschd.msc) - Click "Create Basic Task"
- Name: "Untis Watcher"
- Trigger: "When I log on"
- Action: "Start a program"
- Program:
C:\Users\ayon1xw\Documents\GitHub\Untis-watcher\venv\Scripts\pythonw.exe - Arguments:
main.py - Start in:
C:\Users\ayon1xw\Documents\GitHub\Untis-watcher - Finish
Using screen (simple):
screen -S untis-watcher
python main.py
# Press Ctrl+A, then D to detach
# To reattach: screen -r untis-watcherUsing systemd (auto-start):
Create a service file:
sudo nano /etc/systemd/system/untis-watcher.serviceAdd this content:
[Unit]
Description=Untis Watcher Bot
After=network.target
[Service]
Type=simple
User=youruser
WorkingDirectory=/path/to/Untis-watcher
ExecStart=/path/to/Untis-watcher/venv/bin/python main.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl enable untis-watcher
sudo systemctl start untis-watcher
sudo systemctl status untis-watcherCan be containerized for easier deployment on cloud platforms.
You can build a standalone Windows executable using PyInstaller:
python build_exe.pyOr use the batch file:
.\build.batThe executable will be created in the dist/ folder. Make sure to place your .env file in the same directory as the executable.
Note: The UntisWatcher.spec file includes all necessary hidden imports including dotenv to ensure the executable works correctly.
- Check that
UNTIS_SCHOOLis the short slug (e.g.,school-name), not the full name - Verify your
UNTIS_ELEMENT_IDis correct - Make sure your WebUntis credentials are valid
- Ensure
TELEGRAM_CHAT_IDis your personal ID, not the bot's ID - Make sure you've started a chat with your bot first (send any message to it)
- If you see
[WARNING] Could not send startup greeting, yourTELEGRAM_TOKENis invalid — create a new bot via@BotFatherand update.env
Logs will show:
[WARNING] Could not send startup greeting: The token `<TELEGRAM_TOKEN>` was rejected by the server.
Note: The actual token value is automatically redacted from logs for security. To fix:
- Open
@BotFatherin Telegram →/mybots→ select your bot →API Token - Copy the token and update
TELEGRAM_TOKENin.env
- Verify your GitHub token has
models:readpermissions - Check that
AI_MODELis set togpt-5or another valid model - Ensure you're within GitHub Models rate limits
- The bot only notifies on deterministic changes between
state.jsonand the latest WebUntis fetch, not on every poll or restart - Check that
POLL_INTERVALisn't too long - Verify the timetable data is being fetched correctly
- This has been fixed in the latest version by adding
dotenvtohiddenimportsinUntisWatcher.spec - Rebuild the executable using
python build_exe.py
Untis-watcher/
├── main.py # Entry point and stateful watcher loop
├── timetable.py # WebUntis API integration (JSON-RPC)
├── detector.py # Change detection logic
├── ai.py # GitHub Models integration
├── notifier.py # Telegram notifications
├── storage.py # Persistent timetable storage
├── config.py # Environment variable loading
├── requirements.txt # Python dependencies
├── .env # Configuration (not in git)
└── state.json # Last known WebUntis state (not in git; generated on first run)
- Authentication: Logs into WebUntis using a WebUntis-style JSON-RPC session (or optional REST bearer token credentials)
- State Loading: Reads the previous WebUntis snapshot from
state.jsonif it exists - Fetching: Validates the session, retrieves the weekly timetable for your student ID, and normalizes lesson fields
- Deep Comparison: Normalizes previous and current datasets before comparing them deterministically
- Notification: Sends an AI-generated Telegram summary only when real differences exist
- Storage: Overwrites
state.jsonwith the latest data after a successful fetch; failed fetches keep the previous state intact - Secure Logging: All log output automatically redacts
TELEGRAM_TOKEN,UNTIS_PASSWORD, andAI_API_KEYto prevent credential leaks
This repository includes a manual-only workflow at .github/workflows/manual-ci-cd.yml.
- It does not run on push or pull request.
- Trigger it from GitHub: Actions → Manual CI/CD → Run workflow.
- Input
run_live_untis_check:true: run a live WebUntis smoke fetch using GitHub Secretsfalse: skip live API checks
- Input
build_windows_exe:true: after CI passes, build/uploadUntisWatcher.exeas an artifactfalse: run CI stages only
- CI 1/3 – Static Checks
- installs dependencies
- compiles all core Python files with
py_compile - verifies core imports
- CI 2/3 – Logic Smoke Tests
- runs detector hash/diff assertions
- runs storage save/load roundtrip assertion
- CI 3/3 – Live WebUntis Smoke Check (optional)
- validates required secrets
- logs in via current timetable path (REST or JSON-RPC based on env)
- fetches timetable once and verifies lesson schema
Required for live WebUntis check:
UNTIS_SERVERUNTIS_SCHOOLUNTIS_USERUNTIS_PASSWORDUNTIS_ELEMENT_ID
Optional but recommended:
UNTIS_ELEMENT_TYPE(defaults to5when omitted)UNTIS_TENANT_IDUNTIS_CLIENT_IDUNTIS_API_PASSWORD
The uploaded artifact appears in the workflow run summary under Artifacts.
- Cancellations (Entfall): Free periods
- Changes (Änderung): Room, teacher, or time modifications
- Exams (Prüfung): Detected by keywords in subject names
- Additions: New lessons added to timetable
- Removals: Lessons removed from timetable
This project uses WebUntis JSON-RPC API endpoints that are not officially documented, plus optional WebUntis REST endpoints when REST credentials are configured. Functionality may break if WebUntis changes its internal API.
Important:
- Use responsibly and ensure compliance with your school's policies
- Keep your credentials secure (never commit
.envto git) - Be mindful of API rate limits
- This is for personal use only
Contributions are welcome! Feel free to:
- Report bugs
- Suggest features
- Submit pull requests
See LICENSE file for details.
If you encounter issues:
- Check the troubleshooting section above
- Review your
.envconfiguration - Open an issue on GitHub with error details
The WebUntis JSON-RPC session flow, timetable options, session validation idea, and cookie handling in this project were improved with inspiration from SchoolUtils/WebUntis, an MIT-licensed Node.js WebUntis API wrapper by Nils Bergmann and contributors. This project is not affiliated with Untis GmbH or SchoolUtils.