Skill: sanwebgit/cpap-prisma-app (version 1.1.0)

--- name: cpap-prisma-app version: 1.1.0 description: Fetches CPAP therapy data from the PrismaAPP API (Löwenstein Medical) and writes a daily Obsidian log note. Use when the user says "CPAP log", "sleep therapy", "CPAP data", "prisma log", "CPAP today", "CPAP yesterday", or when the daily cron triggers CPAP logging. --- # CPAP PrismaAPP Log Skill Automatically fetches CPAP therapy data from the Löwenstein PrismaAPP backend and writes a structured daily Obsidian note. ## When Triggered - User says: "CPAP log", "CPAP data", "write sleep therapy", "prisma log", "CPAP today/yesterday/[date]" - Daily cron job fires (configurable) - User requests a historical backfill ## Prerequisites - Account at `my.prismacloud.com` with a linked Löwenstein CPAP device - Device synced via the PrismaAPP mobile app (data must be uploaded to the server) - Credentials stored in `skills/cpap-prisma-app/config.json` ## Execution ### Step 1: Run the Python script **Yesterday (default):** ```bash python3 ~/.openclaw/workspace/skills/cpap-prisma-app/scripts/fetch-cpap.py ``` **Specific date:** ```bash python3 ~/.openclaw/workspace/skills/cpap-prisma-app/scripts/fetch-cpap.py 2026-04-06 ``` **Backfill all data since first sync:** ```bash python3 ~/.openclaw/workspace/skills/cpap-prisma-app/scripts/fetch-cpap.py --all ``` **Backfill from a specific date:** ```bash python3 ~/.openclaw/workspace/skills/cpap-prisma-app/scripts/fetch-cpap.py --from=2026-03-01 ``` ### Step 2: Confirm output The script prints a summary and writes the note to: ``` <vault_path>/<log_dir>/YYYY-MM-DD.md ``` ## Output Format (Note) The generated note contains: - Frontmatter: date, weekday, tags (including quality tag), source, created timestamp - Summary table: sleep duration, deep sleep, AHI, leakage, snoring %, mask fit %, therapy mode/pressure - Overall quality rating (🟢/🟡/🟠/🔴) derived from AHI, leakage, and deep sleep scores - Detailed sections: Sleep, CPAP Therapy, Snoring & Mask - Navigation links to the previous and next logged day All text is fully localised — see the `locales/` folder. ## Quality Mapping | API value | Label | Meaning | |-----------|-------|---------| | 0 | 🟢 Good | Optimal | | 1 | 🟡 Okay | Acceptable | | 2 | 🔴 Poor | Needs attention | | 3 | 🟠 Borderline | Monitor closely | | 4 | — | No data | ## AHI Reference | AHI | Classification | |-----|---------------| | < 5 | Normal | | 5–15 | Mild | | 15–30 | Moderate | | > 30 | Severe | ## Troubleshooting **No data for date:** - Check whether the device was used that night - Open PrismaAPP on the phone to trigger a Bluetooth sync - Data only appears after the device has synced with the app **Login error:** - Verify credentials in `config.json` - Check that `my.prismacloud.com` is reachable - A password change in PrismaAPP requires a `config.json` update **Wrong API responses (HTML instead of JSON):** - Always use `my.prismacloud.com` as the API base - `bucharest.prismacloud.cc` is the web SPA only — it returns HTML for all paths ## Cron Configuration (daily at 18:17) ``` 17 18 * * * python3 /path/to/skills/cpap-prisma-app/scripts/fetch-cpap.py ```

path: README.md
content: ---
name: cpap-prisma-app
version: 1.1.0
description: Fetches CPAP therapy data from the PrismaAPP API (Löwenstein Medical) and writes a daily Obsidian log note. Use when the user says "CPAP log", "sleep therapy", "CPAP data", "prisma log", "CPAP today", "CPAP yesterday", or when the daily cron triggers CPAP logging.
---

# CPAP PrismaAPP Log Skill

Fetches CPAP therapy data from the Löwenstein PrismaAPP backend API and writes structured daily Obsidian notes.

## Supported Devices

Any Löwenstein Medical CPAP/APAP/BiLevel device paired with the **PrismaAPP** (com.lmt.sleeppatientapp). Tested with the prisma SOFT max.

## Setup

> **Note:** `config.json` contains your personal credentials and is listed in `.gitignore`. Never publish or share it. Only `config.example.json` is tracked.

1. Copy `config.example.json` to `config.json`
2. Fill in your PrismaAPP credentials and vault path
3. Optionally set `"language"` to one of: `en`, `de`, `fr`, `es`, `it`, `pt`, `pl`, `el`, `sr`

```bash
cp config.example.json config.json
# Edit config.json
```

**config.json fields:**

| Field | Description | Default |
|-------|-------------|---------|
| `email` | PrismaAPP account e-mail | required |
| `password` | PrismaAPP account password | required |
| `timezone` | IANA timezone string | `Europe/Berlin` |
| `vault_path` | Absolute path to your Obsidian vault | required |
| `log_dir` | Note output folder relative to vault | `30 Bereiche/Gesundheit/CPAP/Logs` |
| `language` | Locale for note text | `en` |

## Usage

```bash
# Yesterday (default — ideal for a daily cron job):
python3 scripts/fetch-cpap.py

# Specific date:
python3 scripts/fetch-cpap.py 2026-04-06

# Backfill all data since first device sync:
python3 scripts/fetch-cpap.py --all

# Backfill from a specific date:
python3 scripts/fetch-cpap.py --from=2026-03-01
```

## API Reference

All API calls use `https://my.prismacloud.com` as the base URL.

> ⚠️ `bucharest.prismacloud.cc` is the web SPA frontend — it returns HTML for all paths and cannot be used as an API endpoint.

| Endpoint | Description |
|----------|-------------|
| `POST /connect/token` | OAuth2 password grant → access token |
| `GET /api/Dashboard` | Returns `minDate` (first sync) and `serialNumber` |
| `GET /api/Dashboard/week?dateInWeek=YYYY-MM-DD` | Week data (7 days) |

**Authentication** uses OAuth2 Resource Owner Password Credentials (`grant_type=password`) with `Content-Type: application/x-www-form-urlencoded`. The token expires after 3600 seconds.

## Output Format

File: `<vault_path>/<log_dir>/YYYY-MM-DD.md`

```yaml
---
date: 2026-04-06
weekday: Monday
tags: ["health", "cpap", "auto-generated", "good"]
source: PrismaAPP / Löwenstein prisma SOFT max
created: 2026-04-08T18:17
---
```

Sections: Summary table · Sleep · CPAP Therapy · Snoring & Mask · Navigation links

## Metrics Explained

| Metric | Source field | Unit |
|--------|-------------|------|
| Sleep duration | `sleepTimeInMinutes` | min |
| Deep sleep | `deepSleepInMinutes` | min |
| AHI (Apnoea-Hypopnoea Index) | `ahi` | events/h |
| Leakage | `leakage` | L/min |
| Snoring | `snoringPercent` | % of sleep time |
| Mask fit | `maskFitPercentage` | % |
| Pressure | `apapMinPressure` / `apapMaxPressure` | hPa |

## Localisation

Note text is fully localised. Available locales: `en` `de` `fr` `es` `it` `pt` `pl` `el` `sr`

Set `"language": "de"` in `config.json` for German notes, etc.

## Privacy Note

This skill accesses personal medical data from your PrismaAPP account using your own credentials. Data is only read — no writes are made to the API. All data is stored locally in your Obsidian vault.


path: config.example.json
content: email: [REDACTED_SECRET]
password: your_password
api_base: https://my.prismacloud.com
timezone: Europe/Berlin
vault_path: /path/to/your/obsidian/vault
log_dir: 30 Bereiche/Gesundheit/CPAP/Logs
language: en

path: locales/de.json
content: {
  "weekdays": ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"],

  "note_title":      "CPAP Log",
  "callout_title":   "Automatisch generiert",
  "callout_body":    "Dieser Eintrag wurde automatisch aus der PrismaAPP-API (Löwenstein Medical) erzeugt.",

  "summary_heading": "Zusammenfassung",
  "col_metric":      "Metrik",
  "col_value":       "Wert",
  "col_rating":      "Bewertung",

  "row_sleep":       "Schlafzeit",
  "row_deepsleep":   "Tiefschlaf",
  "row_ahi":         "AHI",
  "row_leakage":     "Leckage",
  "row_snoring":     "Schnarchen",
  "row_mask":        "Maskensitz",
  "row_therapy":     "Therapiemodus",

  "overall_prefix":  "Gesamtbewertung",

  "sleep_heading":   "Schlaf",
  "sleep_duration":  "Dauer",
  "sleep_deepsleep": "Tiefschlaf",
  "sleep_goal":      "Ziel",
  "goal_reached":    "✅ erreicht",
  "goal_not_reached":"❌ nicht erreicht",

  "therapy_heading": "CPAP-Therapie",
  "therapy_rating":  "Bewertung",
  "therapy_mode":    "Therapiemodus",
  "pressure_label":  "Druck",

  "snoring_heading": "Schnarchen & Maske",
  "snoring_of_time": "der Schlafzeit",
  "mask_label":      "Maskensitz",
  "mask_perfect":    "🟢 Perfekt",
  "mask_ok":         "🟡 Okay",
  "mask_bad":        "🔴 Schlecht",

  "ahi_0":           "≤ 5 — normal",
  "ahi_1":           "5–15 — leicht erhöht",
  "ahi_2":           "15–30 — mittelgradig erhöht",
  "ahi_3":           "> 30 — schwer erhöht",
  "ahi_4":           "—",

  "q_0":             "🟢 Gut",
  "q_1":             "🟡 Okay",
  "q_2":             "🔴 Schlecht",
  "q_3":             "🟠 Grenzwertig",
  "q_4":             "—",

  "overall_great":   "🟢 Sehr gut",
  "overall_good":    "🟢 Gut",
  "overall_ok":      "🟡 Okay",
  "overall_border":  "🟠 Grenzwertig",
  "overall_bad":     "🔴 Schlecht",
  "overall_none":    "— Keine Daten",

  "tag_great":       "sehr-gut",
  "tag_good":        "gut",
  "tag_ok":          "okay",
  "tag_border":      "grenzwertig",
  "tag_bad":         "schlecht",
  "tag_none":        "keine-daten",

  "no_data_msg":     "Keine Daten für dieses Datum verfügbar (Gerät nicht benutzt oder noch nicht synchronisiert).",

  "fm_weekday_key":  "wochentag",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "gesundheit",
  "tag_auto":        "automatisch"
}


path: locales/el.json
content: {
  "weekdays": ["Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο", "Κυριακή"],

  "note_title":      "Ημερολόγιο CPAP",
  "callout_title":   "Αυτόματη δημιουργία",
  "callout_body":    "Αυτή η σημείωση δημιουργήθηκε αυτόματα από τα δεδομένα του API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Σύνοψη",
  "col_metric":      "Μέτρηση",
  "col_value":       "Τιμή",
  "col_rating":      "Αξιολόγηση",

  "row_sleep":       "Διάρκεια ύπνου",
  "row_deepsleep":   "Βαθύς ύπνος",
  "row_ahi":         "AHI",
  "row_leakage":     "Διαρροή",
  "row_snoring":     "Ροχαλητό",
  "row_mask":        "Εφαρμογή μάσκας",
  "row_therapy":     "Λειτουργία θεραπείας",

  "overall_prefix":  "Συνολική αξιολόγηση",

  "sleep_heading":   "Ύπνος",
  "sleep_duration":  "Διάρκεια",
  "sleep_deepsleep": "Βαθύς ύπνος",
  "sleep_goal":      "Στόχος",
  "goal_reached":    "✅ επιτεύχθηκε",
  "goal_not_reached":"❌ δεν επιτεύχθηκε",

  "therapy_heading": "Θεραπεία CPAP",
  "therapy_rating":  "Αξιολόγηση",
  "therapy_mode":    "Λειτουργία θεραπείας",
  "pressure_label":  "Πίεση",

  "snoring_heading": "Ροχαλητό & Μάσκα",
  "snoring_of_time": "του χρόνου ύπνου",
  "mask_label":      "Εφαρμογή μάσκας",
  "mask_perfect":    "🟢 Τέλειο",
  "mask_ok":         "🟡 Αποδεκτό",
  "mask_bad":        "🔴 Κακό",

  "ahi_0":           "≤ 5 — φυσιολογικό",
  "ahi_1":           "5–15 — ελαφρά αυξημένο",
  "ahi_2":           "15–30 — μέτρια αυξημένο",
  "ahi_3":           "> 30 — σοβαρά αυξημένο",
  "ahi_4":           "—",

  "q_0":             "🟢 Καλό",
  "q_1":             "🟡 Αποδεκτό",
  "q_2":             "🔴 Κακό",
  "q_3":             "🟠 Οριακό",
  "q_4":             "—",

  "overall_great":   "🟢 Εξαιρετικό",
  "overall_good":    "🟢 Καλό",
  "overall_ok":      "🟡 Αποδεκτό",
  "overall_border":  "🟠 Οριακό",
  "overall_bad":     "🔴 Κακό",
  "overall_none":    "— Χωρίς δεδομένα",

  "tag_great":       "εξαιρετικο",
  "tag_good":        "καλο",
  "tag_ok":          "αποδεκτο",
  "tag_border":      "οριακο",
  "tag_bad":         "κακο",
  "tag_none":        "χωρις-δεδομενα",

  "no_data_msg":     "Δεν υπάρχουν δεδομένα για αυτή την ημερομηνία (η συσκευή δεν χρησιμοποιήθηκε ή δεν έχει συγχρονιστεί ακόμα).",

  "fm_weekday_key":  "ημέρα",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "υγεία",
  "tag_auto":        "αυτόματο"
}


path: locales/en.json
content: {
  "weekdays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],

  "note_title":      "CPAP Log",
  "callout_title":   "Auto-generated",
  "callout_body":    "This note was automatically created from PrismaAPP API data (Löwenstein Medical).",

  "summary_heading": "Summary",
  "col_metric":      "Metric",
  "col_value":       "Value",
  "col_rating":      "Rating",

  "row_sleep":       "Sleep duration",
  "row_deepsleep":   "Deep sleep",
  "row_ahi":         "AHI",
  "row_leakage":     "Leakage",
  "row_snoring":     "Snoring",
  "row_mask":        "Mask fit",
  "row_therapy":     "Therapy mode",

  "overall_prefix":  "Overall rating",

  "sleep_heading":   "Sleep",
  "sleep_duration":  "Duration",
  "sleep_deepsleep": "Deep sleep",
  "sleep_goal":      "Goal",
  "goal_reached":    "✅ reached",
  "goal_not_reached":"❌ not reached",

  "therapy_heading": "CPAP Therapy",
  "therapy_rating":  "Rating",
  "therapy_mode":    "Therapy mode",
  "pressure_label":  "Pressure",

  "snoring_heading": "Snoring & Mask",
  "snoring_of_time": "of sleep time",
  "mask_label":      "Mask fit",
  "mask_perfect":    "🟢 Perfect",
  "mask_ok":         "🟡 Okay",
  "mask_bad":        "🔴 Poor",

  "ahi_0":           "≤ 5 — normal",
  "ahi_1":           "5–15 — mildly elevated",
  "ahi_2":           "15–30 — moderately elevated",
  "ahi_3":           "> 30 — severely elevated",
  "ahi_4":           "—",

  "q_0":             "🟢 Good",
  "q_1":             "🟡 Okay",
  "q_2":             "🔴 Poor",
  "q_3":             "🟠 Borderline",
  "q_4":             "—",

  "overall_great":   "🟢 Excellent",
  "overall_good":    "🟢 Good",
  "overall_ok":      "🟡 Okay",
  "overall_border":  "🟠 Borderline",
  "overall_bad":     "🔴 Poor",
  "overall_none":    "— No data",

  "tag_great":       "excellent",
  "tag_good":        "good",
  "tag_ok":          "okay",
  "tag_border":      "borderline",
  "tag_bad":         "poor",
  "tag_none":        "no-data",

  "no_data_msg":     "No data available for this date (device not used or not yet synced).",

  "fm_weekday_key":  "weekday",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "health",
  "tag_auto":        "auto-generated"
}


path: locales/es.json
content: {
  "weekdays": ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"],

  "note_title":      "Registro CPAP",
  "callout_title":   "Generado automáticamente",
  "callout_body":    "Esta nota fue creada automáticamente a partir de los datos de la API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Resumen",
  "col_metric":      "Métrica",
  "col_value":       "Valor",
  "col_rating":      "Evaluación",

  "row_sleep":       "Duración del sueño",
  "row_deepsleep":   "Sueño profundo",
  "row_ahi":         "IAH",
  "row_leakage":     "Fuga",
  "row_snoring":     "Ronquidos",
  "row_mask":        "Ajuste de mascarilla",
  "row_therapy":     "Modo de terapia",

  "overall_prefix":  "Evaluación global",

  "sleep_heading":   "Sueño",
  "sleep_duration":  "Duración",
  "sleep_deepsleep": "Sueño profundo",
  "sleep_goal":      "Objetivo",
  "goal_reached":    "✅ alcanzado",
  "goal_not_reached":"❌ no alcanzado",

  "therapy_heading": "Terapia CPAP",
  "therapy_rating":  "Evaluación",
  "therapy_mode":    "Modo de terapia",
  "pressure_label":  "Presión",

  "snoring_heading": "Ronquidos & Mascarilla",
  "snoring_of_time": "del tiempo de sueño",
  "mask_label":      "Ajuste de mascarilla",
  "mask_perfect":    "🟢 Perfecto",
  "mask_ok":         "🟡 Correcto",
  "mask_bad":        "🔴 Malo",

  "ahi_0":           "≤ 5 — normal",
  "ahi_1":           "5–15 — levemente elevado",
  "ahi_2":           "15–30 — moderadamente elevado",
  "ahi_3":           "> 30 — gravemente elevado",
  "ahi_4":           "—",

  "q_0":             "🟢 Bueno",
  "q_1":             "🟡 Correcto",
  "q_2":             "🔴 Malo",
  "q_3":             "🟠 Límite",
  "q_4":             "—",

  "overall_great":   "🟢 Excelente",
  "overall_good":    "🟢 Bueno",
  "overall_ok":      "🟡 Correcto",
  "overall_border":  "🟠 Límite",
  "overall_bad":     "🔴 Malo",
  "overall_none":    "— Sin datos",

  "tag_great":       "excelente",
  "tag_good":        "bueno",
  "tag_ok":          "correcto",
  "tag_border":      "limite",
  "tag_bad":         "malo",
  "tag_none":        "sin-datos",

  "no_data_msg":     "No hay datos disponibles para esta fecha (dispositivo no utilizado o aún no sincronizado).",

  "fm_weekday_key":  "día",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "salud",
  "tag_auto":        "auto-generado"
}


path: locales/fr.json
content: {
  "weekdays": ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"],

  "note_title":      "Journal CPAP",
  "callout_title":   "Généré automatiquement",
  "callout_body":    "Cette note a été créée automatiquement à partir des données de l'API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Résumé",
  "col_metric":      "Métrique",
  "col_value":       "Valeur",
  "col_rating":      "Évaluation",

  "row_sleep":       "Durée de sommeil",
  "row_deepsleep":   "Sommeil profond",
  "row_ahi":         "IAH",
  "row_leakage":     "Fuite",
  "row_snoring":     "Ronflement",
  "row_mask":        "Ajustement masque",
  "row_therapy":     "Mode de thérapie",

  "overall_prefix":  "Évaluation globale",

  "sleep_heading":   "Sommeil",
  "sleep_duration":  "Durée",
  "sleep_deepsleep": "Sommeil profond",
  "sleep_goal":      "Objectif",
  "goal_reached":    "✅ atteint",
  "goal_not_reached":"❌ non atteint",

  "therapy_heading": "Thérapie CPAP",
  "therapy_rating":  "Évaluation",
  "therapy_mode":    "Mode de thérapie",
  "pressure_label":  "Pression",

  "snoring_heading": "Ronflement & Masque",
  "snoring_of_time": "du temps de sommeil",
  "mask_label":      "Ajustement masque",
  "mask_perfect":    "🟢 Parfait",
  "mask_ok":         "🟡 Correct",
  "mask_bad":        "🔴 Mauvais",

  "ahi_0":           "≤ 5 — normal",
  "ahi_1":           "5–15 — légèrement élevé",
  "ahi_2":           "15–30 — modérément élevé",
  "ahi_3":           "> 30 — sévèrement élevé",
  "ahi_4":           "—",

  "q_0":             "🟢 Bien",
  "q_1":             "🟡 Correct",
  "q_2":             "🔴 Mauvais",
  "q_3":             "🟠 Limite",
  "q_4":             "—",

  "overall_great":   "🟢 Excellent",
  "overall_good":    "🟢 Bien",
  "overall_ok":      "🟡 Correct",
  "overall_border":  "🟠 Limite",
  "overall_bad":     "🔴 Mauvais",
  "overall_none":    "— Aucune donnée",

  "tag_great":       "excellent",
  "tag_good":        "bien",
  "tag_ok":          "correct",
  "tag_border":      "limite",
  "tag_bad":         "mauvais",
  "tag_none":        "pas-de-donnees",

  "no_data_msg":     "Aucune donnée disponible pour cette date (appareil non utilisé ou pas encore synchronisé).",

  "fm_weekday_key":  "jour",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "santé",
  "tag_auto":        "auto-généré"
}


path: locales/it.json
content: {
  "weekdays": ["Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato", "Domenica"],

  "note_title":      "Diario CPAP",
  "callout_title":   "Generato automaticamente",
  "callout_body":    "Questa nota è stata creata automaticamente dai dati dell'API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Riepilogo",
  "col_metric":      "Metrica",
  "col_value":       "Valore",
  "col_rating":      "Valutazione",

  "row_sleep":       "Durata del sonno",
  "row_deepsleep":   "Sonno profondo",
  "row_ahi":         "AHI",
  "row_leakage":     "Perdita",
  "row_snoring":     "Russamento",
  "row_mask":        "Tenuta maschera",
  "row_therapy":     "Modalità terapia",

  "overall_prefix":  "Valutazione complessiva",

  "sleep_heading":   "Sonno",
  "sleep_duration":  "Durata",
  "sleep_deepsleep": "Sonno profondo",
  "sleep_goal":      "Obiettivo",
  "goal_reached":    "✅ raggiunto",
  "goal_not_reached":"❌ non raggiunto",

  "therapy_heading": "Terapia CPAP",
  "therapy_rating":  "Valutazione",
  "therapy_mode":    "Modalità terapia",
  "pressure_label":  "Pressione",

  "snoring_heading": "Russamento & Maschera",
  "snoring_of_time": "del tempo di sonno",
  "mask_label":      "Tenuta maschera",
  "mask_perfect":    "🟢 Perfetto",
  "mask_ok":         "🟡 Accettabile",
  "mask_bad":        "🔴 Scarso",

  "ahi_0":           "≤ 5 — normale",
  "ahi_1":           "5–15 — lievemente elevato",
  "ahi_2":           "15–30 — moderatamente elevato",
  "ahi_3":           "> 30 — gravemente elevato",
  "ahi_4":           "—",

  "q_0":             "🟢 Buono",
  "q_1":             "🟡 Accettabile",
  "q_2":             "🔴 Scarso",
  "q_3":             "🟠 Limite",
  "q_4":             "—",

  "overall_great":   "🟢 Eccellente",
  "overall_good":    "🟢 Buono",
  "overall_ok":      "🟡 Accettabile",
  "overall_border":  "🟠 Limite",
  "overall_bad":     "🔴 Scarso",
  "overall_none":    "— Nessun dato",

  "tag_great":       "eccellente",
  "tag_good":        "buono",
  "tag_ok":          "accettabile",
  "tag_border":      "limite",
  "tag_bad":         "scarso",
  "tag_none":        "nessun-dato",

  "no_data_msg":     "Nessun dato disponibile per questa data (dispositivo non utilizzato o non ancora sincronizzato).",

  "fm_weekday_key":  "giorno",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "salute",
  "tag_auto":        "auto-generato"
}


path: locales/pl.json
content: {
  "weekdays": ["Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota", "Niedziela"],

  "note_title":      "Dziennik CPAP",
  "callout_title":   "Wygenerowano automatycznie",
  "callout_body":    "Ta notatka została utworzona automatycznie na podstawie danych API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Podsumowanie",
  "col_metric":      "Metryka",
  "col_value":       "Wartość",
  "col_rating":      "Ocena",

  "row_sleep":       "Czas snu",
  "row_deepsleep":   "Sen głęboki",
  "row_ahi":         "AHI",
  "row_leakage":     "Wyciek",
  "row_snoring":     "Chrapanie",
  "row_mask":        "Dopasowanie maski",
  "row_therapy":     "Tryb terapii",

  "overall_prefix":  "Ocena ogólna",

  "sleep_heading":   "Sen",
  "sleep_duration":  "Czas trwania",
  "sleep_deepsleep": "Sen głęboki",
  "sleep_goal":      "Cel",
  "goal_reached":    "✅ osiągnięty",
  "goal_not_reached":"❌ nie osiągnięty",

  "therapy_heading": "Terapia CPAP",
  "therapy_rating":  "Ocena",
  "therapy_mode":    "Tryb terapii",
  "pressure_label":  "Ciśnienie",

  "snoring_heading": "Chrapanie & Maska",
  "snoring_of_time": "czasu snu",
  "mask_label":      "Dopasowanie maski",
  "mask_perfect":    "🟢 Idealne",
  "mask_ok":         "🟡 Akceptowalne",
  "mask_bad":        "🔴 Złe",

  "ahi_0":           "≤ 5 — normalne",
  "ahi_1":           "5–15 — lekko podwyższone",
  "ahi_2":           "15–30 — umiarkowanie podwyższone",
  "ahi_3":           "> 30 — znacznie podwyższone",
  "ahi_4":           "—",

  "q_0":             "🟢 Dobre",
  "q_1":             "🟡 Akceptowalne",
  "q_2":             "🔴 Złe",
  "q_3":             "🟠 Graniczne",
  "q_4":             "—",

  "overall_great":   "🟢 Doskonałe",
  "overall_good":    "🟢 Dobre",
  "overall_ok":      "🟡 Akceptowalne",
  "overall_border":  "🟠 Graniczne",
  "overall_bad":     "🔴 Złe",
  "overall_none":    "— Brak danych",

  "tag_great":       "doskonale",
  "tag_good":        "dobre",
  "tag_ok":          "akceptowalne",
  "tag_border":      "graniczne",
  "tag_bad":         "zle",
  "tag_none":        "brak-danych",

  "no_data_msg":     "Brak danych dla tej daty (urządzenie nieużywane lub jeszcze nie zsynchronizowane).",

  "fm_weekday_key":  "dzień",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "zdrowie",
  "tag_auto":        "auto-generowane"
}


path: locales/pt.json
content: {
  "weekdays": ["Segunda-feira", "Terça-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado", "Domingo"],

  "note_title":      "Diário CPAP",
  "callout_title":   "Gerado automaticamente",
  "callout_body":    "Esta nota foi criada automaticamente a partir dos dados da API PrismaAPP (Löwenstein Medical).",

  "summary_heading": "Resumo",
  "col_metric":      "Métrica",
  "col_value":       "Valor",
  "col_rating":      "Avaliação",

  "row_sleep":       "Duração do sono",
  "row_deepsleep":   "Sono profundo",
  "row_ahi":         "AHI",
  "row_leakage":     "Vazamento",
  "row_snoring":     "Ronco",
  "row_mask":        "Ajuste da máscara",
  "row_therapy":     "Modo de terapia",

  "overall_prefix":  "Avaliação geral",

  "sleep_heading":   "Sono",
  "sleep_duration":  "Duração",
  "sleep_deepsleep": "Sono profundo",
  "sleep_goal":      "Meta",
  "goal_reached":    "✅ alcançada",
  "goal_not_reached":"❌ não alcançada",

  "therapy_heading": "Terapia CPAP",
  "therapy_rating":  "Avaliação",
  "therapy_mode":    "Modo de terapia",
  "pressure_label":  "Pressão",

  "snoring_heading": "Ronco & Máscara",
  "snoring_of_time": "do tempo de sono",
  "mask_label":      "Ajuste da máscara",
  "mask_perfect":    "🟢 Perfeito",
  "mask_ok":         "🟡 Aceitável",
  "mask_bad":        "🔴 Ruim",

  "ahi_0":           "≤ 5 — normal",
  "ahi_1":           "5–15 — levemente elevado",
  "ahi_2":           "15–30 — moderadamente elevado",
  "ahi_3":           "> 30 — gravemente elevado",
  "ahi_4":           "—",

  "q_0":             "🟢 Bom",
  "q_1":             "🟡 Aceitável",
  "q_2":             "🔴 Ruim",
  "q_3":             "🟠 Limite",
  "q_4":             "—",

  "overall_great":   "🟢 Excelente",
  "overall_good":    "🟢 Bom",
  "overall_ok":      "🟡 Aceitável",
  "overall_border":  "🟠 Limite",
  "overall_bad":     "🔴 Ruim",
  "overall_none":    "— Sem dados",

  "tag_great":       "excelente",
  "tag_good":        "bom",
  "tag_ok":          "aceitavel",
  "tag_border":      "limite",
  "tag_bad":         "ruim",
  "tag_none":        "sem-dados",

  "no_data_msg":     "Nenhum dado disponível para esta data (dispositivo não utilizado ou ainda não sincronizado).",

  "fm_weekday_key":  "dia",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "saúde",
  "tag_auto":        "auto-gerado"
}


path: locales/sr.json
content: {
  "weekdays": ["Ponedeljak", "Utorak", "Sreda", "Četvrtak", "Petak", "Subota", "Nedelja"],

  "note_title":      "CPAP Dnevnik",
  "callout_title":   "Automatski generisano",
  "callout_body":    "Ova beleška je automatski kreirana iz podataka PrismaAPP API (Löwenstein Medical).",

  "summary_heading": "Pregled",
  "col_metric":      "Metrika",
  "col_value":       "Vrednost",
  "col_rating":      "Ocena",

  "row_sleep":       "Trajanje sna",
  "row_deepsleep":   "Duboki san",
  "row_ahi":         "AHI",
  "row_leakage":     "Curenje",
  "row_snoring":     "Hrkanje",
  "row_mask":        "Prianjanje maske",
  "row_therapy":     "Način terapije",

  "overall_prefix":  "Ukupna ocena",

  "sleep_heading":   "San",
  "sleep_duration":  "Trajanje",
  "sleep_deepsleep": "Duboki san",
  "sleep_goal":      "Cilj",
  "goal_reached":    "✅ dostignut",
  "goal_not_reached":"❌ nije dostignut",

  "therapy_heading": "CPAP Terapija",
  "therapy_rating":  "Ocena",
  "therapy_mode":    "Način terapije",
  "pressure_label":  "Pritisak",

  "snoring_heading": "Hrkanje & Maska",
  "snoring_of_time": "vremena sna",
  "mask_label":      "Prianjanje maske",
  "mask_perfect":    "🟢 Savršeno",
  "mask_ok":         "🟡 Prihvatljivo",
  "mask_bad":        "🔴 Loše",

  "ahi_0":           "≤ 5 — normalno",
  "ahi_1":           "5–15 — blago povišeno",
  "ahi_2":           "15–30 — umereno povišeno",
  "ahi_3":           "> 30 — jako povišeno",
  "ahi_4":           "—",

  "q_0":             "🟢 Dobro",
  "q_1":             "🟡 Prihvatljivo",
  "q_2":             "🔴 Loše",
  "q_3":             "🟠 Granično",
  "q_4":             "—",

  "overall_great":   "🟢 Odlično",
  "overall_good":    "🟢 Dobro",
  "overall_ok":      "🟡 Prihvatljivo",
  "overall_border":  "🟠 Granično",
  "overall_bad":     "🔴 Loše",
  "overall_none":    "— Nema podataka",

  "tag_great":       "odlicno",
  "tag_good":        "dobro",
  "tag_ok":          "prihvatljivo",
  "tag_border":      "granicno",
  "tag_bad":         "lose",
  "tag_none":        "nema-podataka",

  "no_data_msg":     "Nema podataka za ovaj datum (uređaj nije korišćen ili još nije sinhronizovan).",

  "fm_weekday_key":  "dan",
  "fm_source":       "PrismaAPP / Löwenstein prisma SOFT max",
  "tag_health":      "zdravlje",
  "tag_auto":        "auto-generisano"
}


path: scripts/fetch-cpap.py
content: #!/usr/bin/env python3
"""
PrismaAPP → Obsidian CPAP Log
Fetches Löwenstein CPAP therapy data and writes a structured daily note.

Usage:
  fetch-cpap.py                     # yesterday (default)
  fetch-cpap.py 2026-04-06          # specific date
  fetch-cpap.py --all               # backfill all data since first sync
  fetch-cpap.py --from=2026-03-01   # backfill from a specific date
"""

import sys
import json
import urllib.request
import urllib.parse
import urllib.error
from datetime import date, timedelta, datetime
from pathlib import Path
from zoneinfo import ZoneInfo

# ── Paths ──────────────────────────────────────────────────────────────────────
SKILL_DIR   = Path(__file__).parent.parent
CONFIG_FILE = SKILL_DIR / "config.json"
LOCALES_DIR = SKILL_DIR / "locales"

THERAPY_MODE = {1: "APAP", 2: "CPAP", 3: "BiLevel", None: "—"}

# ── Config & Locale ────────────────────────────────────────────────────────────
def load_locale(language: str) -> dict:
    locale_file = LOCALES_DIR / f"{language}.json"
    if not locale_file.exists():
        print(f"⚠ Locale '{language}' not found, falling back to 'en'")
        locale_file = LOCALES_DIR / "en.json"
    with open(locale_file, encoding="utf-8") as f:
        return json.load(f)

# ── HTTP helpers ───────────────────────────────────────────────────────────────
def http_post_form(url: str, fields: dict) -> dict:
    data = urllib.parse.urlencode(fields).encode()
    req  = urllib.request.Request(url, data=data,
           headers={"Content-Type": "application/x-www-form-urlencoded"})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())

def http_get(url: str, token: str) -> dict:
    req = urllib.request.Request(url,
          headers={"Authorization": f"Bearer {token}", "Accept": "application/json"})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())

# ── Auth ───────────────────────────────────────────────────────────────────────
def login(email: str, password: str, api_base: str) -> str:
    resp = http_post_form(f"{api_base}/connect/token", {
        "grant_type":  "password",
        "username":    email,
        "password":    password,
        "scope":       "profile offline_access",
        "tenant":      "patientapp",
        "client_id":   "patient-app-client",
    })
    return resp["access_token"]

# ── API ────────────────────────────────────────────────────────────────────────
def get_dashboard(token: str, api_base: str) -> dict:
    return http_get(f"{api_base}/api/Dashboard", token)

def get_week(token: str, date_in_week: str, api_base: str) -> list[dict]:
    data = http_get(f"{api_base}/api/Dashboard/week?dateInWeek={date_in_week}", token)
    return data.get("days", [])

def get_all_days(token: str, min_date_str: str, api_base: str) -> dict[str, dict]:
    """Load all days with data from min_date to today. Returns dict[date_str → day]."""
    min_date = date.fromisoformat(min_date_str)
    today    = date.today()
    by_date  = {}
    current  = min_date
    while current <= today:
        for day in get_week(token, current.isoformat(), api_base):
            if day.get("isDataReceived") and day["date"] not in by_date:
                by_date[day["date"]] = day
        current += timedelta(weeks=1)
    return dict(sorted(by_date.items()))

def get_day(token: str, target: date, api_base: str) -> dict | None:
    """Load data for exactly one day."""
    for d in get_week(token, target.isoformat(), api_base):
        if d["date"] == target.isoformat() and d.get("isDataReceived"):
            return d
    return None

# ── Formatting ─────────────────────────────────────────────────────────────────
def fmt_min(mins: int | None) -> str:
    if not mins:
        return "—"
    h, m = divmod(mins, 60)
    return f"{h}h {m:02d}m"

def fmt_f(v: float | None, digits: int = 1) -> str:
    return f"{v:.{digits}f}" if v is not None else "—"

def quality_label(q: int, t: dict) -> str:
    return t.get(f"q_{q}", "—")

def ahi_info(q: int, t: dict) -> str:
    return t.get(f"ahi_{q}", "—")

def overall_quality(day: dict, t: dict) -> tuple[str, str]:
    scores = [day.get("ahiQuality", 4), day.get("leakageQuality", 4), day.get("deepSleepQuality", 4)]
    valid  = [s for s in scores if s < 4]
    if not valid:
        return t["overall_none"], t["tag_none"]
    avg = sum(valid) / len(valid)
    if avg <= 0.3:   return t["overall_great"],  t["tag_great"]
    if avg <= 1.0:   return t["overall_good"],   t["tag_good"]
    if avg <= 1.5:   return t["overall_ok"],     t["tag_ok"]
    if avg <= 2.2:   return t["overall_border"], t["tag_border"]
    return                  t["overall_bad"],    t["tag_bad"]

def mask_label(mask: int | None, t: dict) -> str:
    if mask is None:    return "—"
    if mask == 100:     return t["mask_perfect"]
    if mask >= 90:      return t["mask_ok"]
    return                     t["mask_bad"]

# ── Note generation ────────────────────────────────────────────────────────────
def make_note(day: dict, all_dates: list[str], t: dict, tz: ZoneInfo) -> str:
    d       = day["date"]
    dt      = date.fromisoformat(d)
    weekday = t["weekdays"][dt.weekday()]

    ahi     = day["ahi"]
    ahi_q   = day.get("ahiQuality", 4)
    leak    = day["leakage"]
    leak_q  = day.get("leakageQuality", 4)
    sleep   = day["sleepTimeInMinutes"]
    deep    = day["deepSleepInMinutes"]
    deep_q  = day.get("deepSleepQuality", 4)
    snore   = day["snoringPercent"]
    snore_q = day.get("snoringQuality", 4)
    mask    = day.get("maskFitPercentage")
    pmin    = day.get("apapMinPressure", 0)
    pmax    = day.get("apapMaxPressure", 0)
    mode    = THERAPY_MODE.get(day.get("therapyMode"), "—")
    goal    = day.get("goalValueInMinutes", 360)
    goal_ok = day.get("isGoalReached", False)
    created = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")

    # Navigation
    base    = "30 Bereiche/Gesundheit/CPAP/Logs"
    idx     = all_dates.index(d) if d in all_dates else -1
    prev_d  = all_dates[idx - 1] if idx > 0 else None
    next_d  = all_dates[idx + 1] if idx >= 0 and idx < len(all_dates) - 1 else None
    nav_prev = f"[[{base}/{prev_d}|← {prev_d}]]" if prev_d else "—"
    nav_next = f"[[{base}/{next_d}|{next_d} →]]" if next_d else "—"

    # Derived values
    druck       = f"{pmin} hPa" if pmin == pmax else f"{pmin}–{pmax} hPa"
    gesamt, tag = overall_quality(day, t)
    goal_str    = t["goal_reached"] if goal_ok else f"{t['goal_not_reached']} ({fmt_min(goal)})"

    return (
        f"---\n"
        f"date: {d}\n"
        f"{t['fm_weekday_key']}: {weekday}\n"
        f'tags: ["{t["tag_health"]}", "cpap", "{t["tag_auto"]}", "{tag}"]\n'
        f"source: {t['fm_source']}\n"
        f"created: {created}\n"
        f"---\n"
        f"\n"
        f"# 😴 {t['note_title']} — {weekday}, {dt.strftime('%d.%m.%Y')}\n"
        f"\n"
        f"> [!info] {t['callout_title']}\n"
        f"> {t['callout_body']}\n"
        f"\n"
        f"---\n"
        f"\n"
        f"## 📊 {t['summary_heading']}\n"
        f"\n"
        f"| {t['col_metric']} | {t['col_value']} | {t['col_rating']} |\n"
        f"|--------|------|-----------|\n"
        f"| 😴 {t['row_sleep']} | {fmt_min(sleep)} | {goal_str} |\n"
        f"| 🧠 {t['row_deepsleep']} | {fmt_min(deep)} | {quality_label(deep_q, t)} |\n"
        f"| 💨 {t['row_ahi']} | {fmt_f(ahi)} | {quality_label(ahi_q, t)} |\n"
        f"| 💧 {t['row_leakage']} | {fmt_f(leak)} L/min | {quality_label(leak_q, t)} |\n"
        f"| 🔊 {t['row_snoring']} | {snore} % | {quality_label(snore_q, t)} |\n"
        f"| 😷 {t['row_mask']} | {mask if mask is not None else '—'} % | {mask_label(mask, t)} |\n"
        f"| ⚙️ {t['row_therapy']} | {mode} | {t['pressure_label']}: {druck} |\n"
        f"\n"
        f"**{t['overall_prefix']}: {gesamt}**\n"
        f"\n"
        f"---\n"
        f"\n"
        f"## 😴 {t['sleep_heading']}\n"
        f"\n"
        f"- **{t['sleep_duration']}:** {fmt_min(sleep)}  \n"
        f"- **{t['sleep_deepsleep']}:** {fmt_min(deep)} ({quality_label(deep_q, t)})\n"
        f"- **{t['sleep_goal']}:** {fmt_min(goal)} — {t['goal_reached'] if goal_ok else t['goal_not_reached']}\n"
        f"\n"
        f"---\n"
        f"\n"
        f"## 💨 {t['therapy_heading']}\n"
        f"\n"
        f"- **{t['row_ahi']}:** {fmt_f(ahi)} — {ahi_info(ahi_q, t)}  \n"
        f"  {t['therapy_rating']}: {quality_label(ahi_q, t)}\n"
        f"- **{t['row_leakage']}:** {fmt_f(leak)} L/min — {quality_label(leak_q, t)}\n"
        f"- **{t['therapy_mode']}:** {mode}\n"
        f"- **{t['pressure_label']}:** {druck}\n"
        f"\n"
        f"---\n"
        f"\n"
        f"## 🔊 {t['snoring_heading']}\n"
        f"\n"
        f"- **{t['row_snoring']}:** {snore} % {t['snoring_of_time']} — {quality_label(snore_q, t)}\n"
        f"- **{t['mask_label']}:** {mask if mask is not None else '—'} %\n"
        f"\n"
        f"---\n"
        f"\n"
        f"*{nav_prev}  |  {nav_next}*\n"
    )

# ── Main ───────────────────────────────────────────────────────────────────────
def main():
    with open(CONFIG_FILE, encoding="utf-8") as f:
        cfg = json.load(f)

    tz       = ZoneInfo(cfg.get("timezone", "Europe/Berlin"))
    t        = load_locale(cfg.get("language", "en"))
    api_base = cfg.get("api_base", "https://my.prismacloud.com").rstrip("/")
    log_dir  = Path(cfg["vault_path"]) / cfg.get("log_dir", "30 Bereiche/Gesundheit/CPAP/Logs")
    log_dir.mkdir(parents=True, exist_ok=True)

    args     = sys.argv[1:]
    do_all   = "--all" in args
    from_arg = next((a for a in args if a.startswith("--from=")), None)
    date_arg = next((a for a in args if not a.startswith("--")), None)

    print("Logging in ...")
    [REDACTED_SECRET]"email"], cfg["password"], api_base)
    print("✓ Authenticated")

    # ── Single day mode ───────────────────────────────────────────────────────
    if not do_all and not from_arg:
        if date_arg:
            target = date.fromisoformat(date_arg)
        else:
            target = (datetime.now(tz) - timedelta(days=1)).date()

        print(f"Loading data for {target} ...")
        day = get_day(token, target, api_base)

        if not day:
            print(f"⚠ {t['no_data_msg']}")
            sys.exit(0)

        week_dates = sorted(d["date"] for d in get_week(token, target.isoformat(), api_base)
                            if d.get("isDataReceived"))
        note = make_note(day, week_dates, t, tz)

        out = log_dir / f"{target}.md"
        out.write_text(note, encoding="utf-8")
        print(f"✓ Note written: {out}")
        _print_summary(day, t)
        return

    # ── Backfill mode ─────────────────────────────────────────────────────────
    print("Loading dashboard ...")
    dashboard = get_dashboard(token, api_base)
    api_min   = dashboard.get("minDate", str(date.today()))

    start_date = from_arg.split("=", 1)[1] if from_arg else api_min
    if start_date < api_min:
        start_date = api_min

    print(f"Loading all days from {start_date} ...")
    days  = get_all_days(token, start_date, api_base)
    dates = list(days.keys())
    print(f"  {len(days)} days with data found.")

    created = skipped = 0
    for d_str, day in days.items():
        out = log_dir / f"{d_str}.md"
        if out.exists():
            skipped += 1
            continue
        note = make_note(day, dates, t, tz)
        out.write_text(note, encoding="utf-8")
        print(f"  ✓ {d_str}")
        created += 1

    print(f"\nDone: {created} created, {skipped} already existed.")

def _print_summary(day: dict, t: dict):
    gesamt, _ = overall_quality(day, t)
    print(f"\n📊 {day['date']}")
    print(f"   {t['row_sleep']:15} {fmt_min(day.get('sleepTimeInMinutes'))}")
    print(f"   {t['row_deepsleep']:15} {fmt_min(day.get('deepSleepInMinutes'))}")
    print(f"   {t['row_ahi']:15} {fmt_f(day.get('ahi'))}")
    print(f"   {t['row_leakage']:15} {fmt_f(day.get('leakage'))} L/min")
    print(f"   {t['row_snoring']:15} {day.get('snoringPercent', '—')} %")
    print(f"   {t['row_mask']:15} {day.get('maskFitPercentage', '—')} %")
    print(f"   {t['overall_prefix']:15} {gesamt}")

if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)