Hi zusammen,
der Adapter war mir zu unzuverlässig bzw. hatte ich ein Javascript gefunden, dass mir alles für vis in einer Tabelle aufarbeitet. Das war aber nicht immer richtig. Ich habe mit Hilfe von copilot ein Skript gebaut, das mir Screenshots des Stundenplans macht.
# Create a new folder for the script
mkdir webuntis-shot && cd webuntis-shot
# Initialize and install dependencies
npm init -y
npm i playwright dotenv
# Install browsers for Playwright (first time only)
npx playwright install
dann .env Datei erstellen
# .env
WEBUNTIS_USERNAME="user"
WEBUNTIS_PASSWORD="xyz"
HEADLESS="1"
OUTPUT_DIR="/opt/iobroker"
dann die Datei z.B. webuntis.js anlegen
// webuntis-screenshot.js
// Usage: node webuntis-screenshot.js
// Optional env: WEBUNTIS_USERNAME, WEBUNTIS_PASSWORD, HEADLESS, OUTPUT_DIR, TARGET_HASH_FILE
import { chromium } from 'playwright';
import fs from 'fs';
import fsp from 'fs/promises';
import path from 'path';
import 'dotenv/config';
const USERNAME = process.env.WEBUNTIS_USERNAME || 'user';
const PASSWORD = process.env.WEBUNTIS_PASSWORD || 'pass';
const HEADLESS = (process.env.HEADLESS || '1') !== '0';
const OUTPUT_DIR = process.env.OUTPUT_DIR || '/opt/iobroker/iobroker-data/files/vis.0/main/img/';
const BASE_URL = 'https://gss-realschule.webuntis.com/timetable/my-student?date=';
// === NEW: read TARGET_HASH from external file ===
const TARGET_HASH_FILE = process.env.TARGET_HASH_FILE || path.resolve(process.cwd(), 'target-hash.txt');
async function loadTargetHash(filePath) {
const exists = fs.existsSync(filePath);
if (!exists) {
throw new Error(`Target-hash file not found: ${filePath}`);
}
const ext = path.extname(filePath).toLowerCase();
if (ext === '.json') {
const raw = await fsp.readFile(filePath, 'utf-8');
let data;
try {
data = JSON.parse(raw);
} catch (e) {
throw new Error(`Invalid JSON in ${filePath}: ${e.message}`);
}
const hash = (data.TARGET_HASH || '').trim();
if (!hash) throw new Error(`Missing "TARGET_HASH" property in ${filePath}`);
validateHash(hash);
return hash;
} else {
// treat as plain text
const hash = (await fsp.readFile(filePath, 'utf-8')).trim();
validateHash(hash);
return hash;
}
}
function validateHash(hash) {
if (!hash.startsWith('#/')) {
throw new Error(`TARGET_HASH must start with "#/". Got: "${hash}"`);
}
}
// Utility: ensure directory exists (try to create if not)
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const testFile = path.join(dir, '.test_write.tmp');
fs.writeFileSync(testFile, 'ok');
fs.unlinkSync(testFile);
return true;
} catch (e) {
console.error(`[WARN] Cannot write to ${dir}: ${e.message}`);
return false;
}
}
async function tryFill(page, selectors, value, opts = {}) {
for (const sel of selectors) {
try {
const el = await page.$(sel);
if (el) {
await el.fill(value, { timeout: opts.timeout || 3000 });
return true;
}
} catch { /* continue */ }
}
return false;
}
async function tryClick(page, candidates, opts = {}) {
for (const c of candidates) {
try {
if (c.selector) {
await page.click(c.selector, { timeout: opts.timeout || 3000 });
return true;
}
if (c.text) {
const el = page.getByText(c.text, { exact: false });
await el.first().click({ timeout: opts.timeout || 3000 });
return true;
}
} catch { /* continue */ }
}
return false;
}
async function acceptCookies(page) {
const candidates = [
{ text: 'Accept all' }, { text: 'Accept All' }, { text: 'Accept' },
{ text: 'Alle akzeptieren' }, { text: 'Akzeptieren' }, { text: 'Einverstanden' },
{ selector: 'button#onetrust-accept-btn-handler' },
{ selector: 'button[aria-label*="accept"]' },
];
await tryClick(page, candidates, { timeout: 2000 });
}
async function loginIfNeeded(page, username, password) {
const usernameSelectors = [
'input[name="username"]',
'input#username',
'input[name="user"]',
'input[type="text"]',
'input[autocomplete="username"]'
];
const passwordSelectors = [
'input[name="password"]',
'input#password',
'input[type="password"]',
'input[autocomplete="current-password"]'
];
const loginButtonCandidates = [
{ text: 'Log in' }, { text: 'Login' }, { text: 'Anmelden' }, { text: 'Sign in' },
{ selector: 'button[type="submit"]' }
];
let needsLogin = false;
for (const sel of passwordSelectors) {
const el = await page.$(sel);
if (el) { needsLogin = true; break; }
}
if (!needsLogin) {
const clicked = await tryClick(page, [{ text: 'Login' }, { text: 'Anmelden' }, { text: 'Log in' }], { timeout: 2000 });
if (clicked) {
await page.waitForTimeout(1200);
}
for (const sel of passwordSelectors) {
const el = await page.$(sel);
if (el) { needsLogin = true; break; }
}
}
if (!needsLogin) return false;
const uFilled = await tryFill(page, usernameSelectors, username);
const pFilled = await tryFill(page, passwordSelectors, password);
if (!uFilled || !pFilled) {
throw new Error('Could not locate username/password fields to perform login.');
}
const clickedSubmit = await tryClick(page, loginButtonCandidates, { timeout: 3000 });
if (!clickedSubmit) {
for (const sel of passwordSelectors) {
const el = await page.$(sel);
if (el) {
await el.press('Enter');
break;
}
}
}
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
return true;
}
function timestamp() {
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
}
(async () => {
console.log('[INFO] Starting WebUntis screenshot task...');
const canWrite = ensureDir(OUTPUT_DIR);
const outDir = canWrite ? OUTPUT_DIR : process.cwd();
if (!canWrite) {
console.warn(`[WARN] Falling back to current directory: ${outDir}`);
}
// Load external hash
let TARGET_HASH = '';
try {
TARGET_HASH = await loadTargetHash(TARGET_HASH_FILE);
console.log(`[INFO] Loaded TARGET_HASH from ${TARGET_HASH_FILE}: ${TARGET_HASH}`);
} catch (e) {
console.error('[ERROR] Failed to load TARGET_HASH:', e.message);
process.exit(1);
}
const TARGET_URL = `${BASE_URL}${TARGET_HASH}`;
const browser = await chromium.launch({ headless: HEADLESS });
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
const page = await context.newPage();
try {
console.log('[INFO] Navigating to base URL…');
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await acceptCookies(page);
await loginIfNeeded(page, USERNAME, PASSWORD);
console.log('[INFO] Navigating to target timetable URL…');
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForTimeout(4000);
await acceptCookies(page);
const filePath = path.join(outDir, `webuntis-screenshot.png`);
await page.screenshot({ path: filePath, fullPage: true });
console.log(`[SUCCESS] Screenshot saved to: ${filePath}`);
} catch (err) {
console.error('[ERROR]', err.message);
process.exitCode = 1;
} finally {
await context.close();
await browser.close();
}
})();
eigentlich dachte ich, ich muss die URL noch wöchentlich auf die richtige Woche ändern, das macht aber ein redirect. die target-hash.txt Datei ist aber geblieben (gleicher Ordner) - steht "nichts" drin, könnte geändert werden
#/
dann das Skript laufen lassen
node webuntis.js
Bei mir kamen noch ein paar Fehler, ich musste ein paar libraries nachinstallieren.
Somit läuft es bei mir, ich muss jetzt nur noch schauen, ob ich es mit einem blockly und cron triggere oder von debian aus.
[image: 1773492443224-494470b8-8e71-4663-8202-0716112cdf72-grafik.png]
Damit läuft das Skript stündlich
0 * * * * /usr/bin/node /home/mading/webuntis-shot/webuntis.js >> /home/mading/webuntis-shot/cron.log 2>&1