{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "VrZNDCgWJAhb"
   },
   "source": "# QAPU — Elektrik Analizörü Reaktif Analiz & Kestirimci Bakım\n### Offline Narrator ile 4 kitleye özel rapor üretimi (Google Colab)\n\nBu not defteri `qapu.dump` (PostgreSQL 18 custom format) dosyasını **pg_restore gerektirmeden**\ndoğrudan ayrıştırır ve:\n\n1. **Reaktif enerji uygunluk analizi** — EPDK sınırları (endüktif %20 / kapasitif %15), güç faktörü\n2. **Kestirimci bakım** — harmonik (THD), faz dengesizliği, sıcaklık, trend projeksiyonu,\n   IsolationForest ile anomali tespiti, cihaz başına **sağlık skoru (0–100)**\n3. **Offline narrator** — internet/LLM kullanmadan, şablon tabanlı Türkçe raporlar:\n   * 👷 Teknik uzman &nbsp;·&nbsp; 💼 Özel sektör yöneticisi &nbsp;·&nbsp; 🏛️ Kamu yöneticisi &nbsp;·&nbsp; 👤 Vatandaş\n   * Çıktı formatları: **Word (.docx)**, HTML ve Markdown — grafikler gömülü\n\n**Kullanım:** Hücreleri sırayla çalıştırın. 6. hücre dump dosyasını yüklemenizi ister\n(veya `DUMP_PATH` değişkenine Drive yolunu yazın). Çıktılar `raporlar/` klasörüne ve\nindirilebilir bir ZIP'e yazılır.\n\n> Rapor üretimi tamamen deterministiktir; aynı veri her zaman aynı raporu üretir.\n",
   "id": "VrZNDCgWJAhb"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "kkP8Mx8IJAhe",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470014307,
     "user_tz": -180,
     "elapsed": 2270,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "673a2acc-ffc1-4bfe-a6c9-e905a1e33021"
   },
   "execution_count": 1,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Hazır.\n"
     ]
    }
   ],
   "source": "# 1) Kurulum — dump ayrıştırma (zstd), HTML ve Word (.docx) çıktıları için\n!pip -q install zstandard markdown python-docx\nprint(\"Hazır.\")",
   "id": "kkP8Mx8IJAhe"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "6IKMue1ZJAhg",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470017968,
     "user_tz": -180,
     "elapsed": 28,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "18614a40-c754-42c0-ecab-2dfbef66531f"
   },
   "execution_count": 2,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_extract.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_extract.py\n#!/usr/bin/env python3\n\"\"\"Extract selected tables from a PostgreSQL custom-format dump (v1.16, zstd) to CSV.\n\nWorks without pg_restore: parses the TOC and decompresses data blocks directly.\nUsage: python3 extract_tables.py <dump> <outdir>\n\"\"\"\nimport sys, os, io, re, csv, json\nimport zstandard\n\nTARGET_PREFIXES = (\n    'measurements_energy', 'measurements_power', 'measurements_voltage',\n    'measurements_current', 'measurements_device', 'measurements_environment',\n)\nTARGET_EXACT = {'devices', 'streams', 'variables', 'variable_segments', 'sites',\n                'site_device_assignments', 'manufacturer_models'}\n\n\ndef parse_dump(path):\n    f = open(path, 'rb')\n    magic = f.read(5)\n    assert magic == b'PGDMP', 'Not a pg_dump custom archive'\n    vmaj, vmin, vrev = f.read(1)[0], f.read(1)[0], f.read(1)[0]\n    intSize = f.read(1)[0]\n    offSize = f.read(1)[0]\n    fmt = f.read(1)[0]\n\n    def read_int():\n        sign = f.read(1)[0]\n        v = int.from_bytes(f.read(intSize), 'little')\n        return -v if sign else v\n\n    def read_str():\n        n = read_int()\n        if n < 0:\n            return None\n        return f.read(n).decode('utf-8', 'replace')\n\n    compression = f.read(1)[0]  # v>=1.15: 0 none, 1 gzip, 2 lz4, 3 zstd\n    for _ in range(7):\n        read_int()  # created date parts\n    dbname = read_str(); read_str(); read_str()\n\n    toc_count = read_int()\n    entries = []\n    for _ in range(toc_count):\n        e = {}\n        e['dumpId'] = read_int(); e['hadDumper'] = read_int()\n        e['tableoid'] = read_str(); e['oid'] = read_str()\n        e['tag'] = read_str(); e['desc'] = read_str()\n        e['section'] = read_int()\n        e['defn'] = read_str(); e['dropStmt'] = read_str(); e['copyStmt'] = read_str()\n        e['namespace'] = read_str(); e['tablespace'] = read_str(); e['tableam'] = read_str()\n        e['relkind'] = read_int()\n        e['owner'] = read_str(); e['withOids'] = read_str()\n        deps = []\n        while True:\n            d = read_str()\n            if d is None:\n                break\n            deps.append(d)\n        e['deps'] = deps\n        f.read(1 + offSize)  # offset flag + offset\n        entries.append(e)\n    return f, f.tell(), entries, compression, read_int\n\n\ndef columns_from_copy(copy_stmt):\n    m = re.search(r'\\(([^)]*)\\)', copy_stmt or '')\n    if not m:\n        return None\n    return [c.strip().strip('\"') for c in m.group(1).split(',')]\n\n\ndef main(dump_path, outdir):\n    os.makedirs(outdir, exist_ok=True)\n    f, data_start, entries, compression, read_int = parse_dump(dump_path)\n    byid = {e['dumpId']: e for e in entries}\n\n    def wanted(tag):\n        return tag in TARGET_EXACT or any(tag.startswith(p) for p in TARGET_PREFIXES)\n\n    targets = {e['dumpId']: e for e in entries\n               if e['desc'] == 'TABLE DATA' and wanted(e['tag'])}\n    print(f'{len(targets)} table-data entries to extract')\n\n    f.seek(data_start)\n    writers = {}  # base table name -> (file, header_written)\n    found = 0\n    while True:\n        b = f.read(1)\n        if not b:\n            break\n        blktype = b[0]\n        if blktype not in (1, 3, 49, 51):\n            break\n        did = read_int()\n        e = targets.get(did)\n        grab = e is not None\n        if blktype in (3, 51):  # blobs\n            while True:\n                oid = read_int()\n                if oid == 0:\n                    break\n                while True:\n                    n = read_int()\n                    if n <= 0:\n                        break\n                    f.seek(n, 1)\n            continue\n        dobj = zstandard.ZstdDecompressor().decompressobj() if grab else None\n        chunks = []\n        while True:\n            n = read_int()\n            if n <= 0:\n                break\n            buf = f.read(n)\n            if grab:\n                chunks.append(dobj.decompress(buf))\n        if not grab:\n            continue\n        found += 1\n        data = b''.join(chunks).decode('utf-8', 'replace')\n        tag = e['tag']\n        # fold partitions into base table name\n        base = re.sub(r'_(\\d{4}_\\d{2}|default)$', '', tag)\n        cols = columns_from_copy(e['copyStmt'])\n        path = os.path.join(outdir, base + '.csv')\n        new = base not in writers\n        out = open(path, 'a', newline='')\n        w = csv.writer(out)\n        if new and cols:\n            w.writerow(cols)\n            writers[base] = True\n        for line in data.split('\\n'):\n            if not line or line == '\\\\.':\n                continue\n            row = [None if v == '\\\\N' else v for v in line.split('\\t')]\n            w.writerow(row)\n        out.close()\n    print(f'extracted {found} data blocks into {len(writers)} CSVs: {sorted(writers)}')\n\n\nif __name__ == '__main__':\n    main(sys.argv[1], sys.argv[2])\n",
   "id": "6IKMue1ZJAhg"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "ydxxzHrpJAhh",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470027992,
     "user_tz": -180,
     "elapsed": 95,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "157b7826-0e8f-435e-ffd4-1754a9d444bf"
   },
   "execution_count": 3,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_pm.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_pm.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU Elektrik Analizörü — Reaktif Analiz + Kestirimci Bakım + Offline Narrator\n==============================================================================\nEnerji analizörü (MAX78630) verilerinden:\n  1. Reaktif enerji uygunluk analizi (EPDK endüktif %20 / kapasitif %15 sınırları)\n  2. Kestirimci bakım: güç faktörü, harmonik (THD), faz dengesizliği, trend,\n     anomali tespiti (IsolationForest), cihaz sağlık skoru (0-100)\n  3. Offline narrator: 4 hedef kitle için şablon tabanlı Türkçe rapor\n     (teknik uzman, özel sektör yöneticisi, kamu yöneticisi, vatandaş)\nİnternet/LLM gerektirmez; tamamen deterministik şablonlarla çalışır.\n\"\"\"\nimport os, math, json\nimport numpy as np\nimport pandas as pd\n\n# ----------------------------------------------------------------------------\n# KONFİGÜRASYON — eşikler ve standart referansları\n# ----------------------------------------------------------------------------\nCONFIG = {\n    \"ind_limit\": 0.20,      # EPDK: endüktif reaktif / aktif enerji aylık sınırı\n    \"cap_limit\": 0.15,      # EPDK: kapasitif reaktif / aktif enerji aylık sınırı\n    \"pf_target\": 0.95,      # hedef güç faktörü (yük altında)\n    \"thd_v_limit\": 0.08,    # IEEE 519 esinli gerilim THD sınırı (%8)\n    \"thd_i_limit\": 0.15,    # akım THD uyarı eşiği (%15)\n    \"v_imb_limit\": 0.02,    # gerilim dengesizliği sınırı (%2, IEC/NEMA)\n    \"i_imb_limit\": 0.10,    # akım dengesizliği uyarı eşiği (%10)\n    \"pcb_t_limit\": 60.0,    # elektronik sıcaklık uyarı eşiği (°C)\n    \"load_frac\": 0.05,      # \"yük altında\" kabulü: S > p95(S) * 0.05\n    \"min_days_trend\": 7,    # trend için asgari gün\n    \"min_days_iforest\": 10, # anomali modeli için asgari gün\n    \"anomaly_contamination\": 0.08,\n    \"vrms_plausible\": (120.0, 300.0),  # kalibrasyon kontrolü (faz-nötr V)\n}\n\nRISK_BANDS = [(85, \"İYİ\"), (70, \"İZLENMELİ\"), (50, \"RİSKLİ\"), (-1, \"KRİTİK\")]\n\n\ndef _risk(score):\n    for th, name in RISK_BANDS:\n        if score >= th:\n            return name\n    return \"KRİTİK\"\n\n\n# ----------------------------------------------------------------------------\n# 1. VERİ YÜKLEME / HAZIRLIK\n# ----------------------------------------------------------------------------\ndef load_csvs(data_dir):\n    \"\"\"extract_tables ile üretilen CSV'leri okur; tablolar sözlüğü döner.\"\"\"\n    tabs = {}\n    for name in [\"measurements_energy\", \"measurements_power\", \"measurements_voltage\",\n                 \"measurements_current\", \"measurements_device\",\n                 \"measurements_environment\", \"devices\"]:\n        p = os.path.join(data_dir, name + \".csv\")\n        if os.path.exists(p):\n            df = pd.read_csv(p, low_memory=False)\n            if \"device_time\" in df.columns:\n                df[\"device_time\"] = pd.to_datetime(df[\"device_time\"], utc=True, errors=\"coerce\")\n                df = df.dropna(subset=[\"device_time\"])\n                if \"is_valid\" in df.columns:\n                    df = df[df[\"is_valid\"].astype(str).str.lower().isin([\"t\", \"true\", \"1\"])]\n            tabs[name] = df\n    return tabs\n\n\ndef _thd(df, prefix, phases=(\"r\", \"s\", \"t\"), harms=(3, 5, 7, 9)):\n    \"\"\"sqrt(Σ harmonik²)/temel — faz başına; fazların ortalaması.\"\"\"\n    vals = []\n    for ph in phases:\n        fund = df.get(f\"{prefix}fund_{ph}\")\n        if fund is None:\n            continue\n        h2 = sum(df.get(f\"{prefix}harm_{ph}_{h}\", pd.Series(np.nan, index=df.index)) ** 2\n                 for h in harms)\n        thd = np.sqrt(h2) / fund.where(fund > 1e-6)\n        vals.append(thd)\n    if not vals:\n        return pd.Series(np.nan, index=df.index)\n    return pd.concat(vals, axis=1).mean(axis=1)\n\n\ndef _imbalance(df, cols):\n    \"\"\"Üç faz dengesizliği: max |x_i - ort| / ort (ort anlamlıysa).\"\"\"\n    sub = df[list(cols)]\n    mean = sub.mean(axis=1)\n    dev = sub.sub(mean, axis=0).abs().max(axis=1)\n    return (dev / mean.where(mean > 1e-6)).rename(\"imb\")\n\n\ndef build_daily(tabs, cfg=CONFIG):\n    \"\"\"Cihaz × gün bazında özellik matrisi üretir.\"\"\"\n    frames = []\n\n    e = tabs.get(\"measurements_energy\")\n    if e is not None and len(e):\n        e = e.copy()\n        for c in [\"ae_r\", \"ae_s\", \"ae_t\", \"re_l_r\", \"re_l_s\", \"re_l_t\",\n                  \"re_g_r\", \"re_g_s\", \"re_g_t\"]:\n            if c in e:\n                e[c] = pd.to_numeric(e[c], errors=\"coerce\").clip(lower=0)  # negatifler geçersiz\n        e[\"ae\"] = e[[\"ae_r\", \"ae_s\", \"ae_t\"]].sum(axis=1, min_count=1)\n        e[\"re_l\"] = e[[\"re_l_r\", \"re_l_s\", \"re_l_t\"]].sum(axis=1, min_count=1)\n        e[\"re_g\"] = e[[\"re_g_r\", \"re_g_s\", \"re_g_t\"]].sum(axis=1, min_count=1)\n        g = (e.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\")[[\"ae\", \"re_l\", \"re_g\"]].sum(min_count=1))\n        frames.append(g)\n\n    p = tabs.get(\"measurements_power\")\n    if p is not None and len(p):\n        p = p.copy()\n        for c in p.columns:\n            if c not in (\"device_id\", \"device_time\", \"is_valid\"):\n                p[c] = pd.to_numeric(p[c], errors=\"coerce\")\n        p[\"s_tot\"] = p[[\"s_r\", \"s_s\", \"s_t\"]].abs().sum(axis=1, min_count=1)\n        p[\"pf_abs\"] = p[[\"pf_r\", \"pf_s\", \"pf_t\"]].abs().mean(axis=1)\n        # yük eşiği: cihaz bazında p95'in %5'i\n        thr = p.groupby(\"device_id\")[\"s_tot\"].transform(lambda s: s.quantile(0.95) * cfg[\"load_frac\"])\n        p[\"loaded\"] = p[\"s_tot\"] > thr\n        p[\"pf_loaded\"] = p[\"pf_abs\"].where(p[\"loaded\"])\n        p[\"fq_dev\"] = (p[\"fq\"] - 50.0).abs().where(p[\"fq\"].between(40, 70))\n        g = (p.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(pf_loaded=(\"pf_loaded\", \"mean\"),\n                                   load_hours=(\"loaded\", \"sum\"),\n                                   s_max=(\"s_tot\", \"max\"),\n                                   fq_dev=(\"fq_dev\", \"mean\")))\n        g[\"load_hours\"] *= 5 / 60.0  # ~5 dk örnekleme → saat\n        frames.append(g)\n\n    v = tabs.get(\"measurements_voltage\")\n    if v is not None and len(v):\n        v = v.copy()\n        for c in v.columns:\n            if c not in (\"device_id\", \"device_time\", \"is_valid\"):\n                v[c] = pd.to_numeric(v[c], errors=\"coerce\")\n        v[\"thd_v\"] = _thd(v, \"v\")\n        v[\"v_imb\"] = _imbalance(v, [\"vrms_r\", \"vrms_s\", \"vrms_t\"])\n        v[\"vrms_avg\"] = v[[\"vrms_r\", \"vrms_s\", \"vrms_t\"]].mean(axis=1)\n        g = (v.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(thd_v=(\"thd_v\", \"median\"),\n                                   v_imb=(\"v_imb\", \"median\"),\n                                   vrms_med=(\"vrms_avg\", \"median\")))\n        frames.append(g)\n\n    c = tabs.get(\"measurements_current\")\n    if c is not None and len(c):\n        c = c.copy()\n        for col in c.columns:\n            if col not in (\"device_id\", \"device_time\", \"is_valid\"):\n                c[col] = pd.to_numeric(c[col], errors=\"coerce\")\n        c[\"thd_i\"] = _thd(c, \"i\")\n        c[\"i_imb\"] = _imbalance(c, [\"irms_r\", \"irms_s\", \"irms_t\"])\n        c[\"irms_avg\"] = c[[\"irms_r\", \"irms_s\", \"irms_t\"]].mean(axis=1)\n        # yüksüzken dengesizlik anlamsız → medyan akımın %10'u altını at\n        thr = c.groupby(\"device_id\")[\"irms_avg\"].transform(lambda s: max(s.quantile(0.9) * 0.1, 0.2))\n        c.loc[c[\"irms_avg\"] < thr, [\"i_imb\", \"thd_i\"]] = np.nan\n        g = (c.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(thd_i=(\"thd_i\", \"median\"),\n                                   i_imb=(\"i_imb\", \"median\"),\n                                   irms_max=(\"irms_avg\", \"max\")))\n        frames.append(g)\n\n    env = tabs.get(\"measurements_environment\")\n    if env is not None and len(env):\n        env = env.copy()\n        env[\"pcb_t\"] = pd.to_numeric(env.get(\"pcb_t\"), errors=\"coerce\")\n        g = (env.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(pcb_t_max=(\"pcb_t\", \"max\")))\n        frames.append(g)\n\n    d = tabs.get(\"measurements_device\")\n    if d is not None and len(d):\n        d = d.copy()\n        for col in [\"rssi\", \"b_soc\"]:\n            if col in d:\n                d[col] = pd.to_numeric(d[col], errors=\"coerce\")\n        g = (d.set_index(\"device_time\").groupby(\"device_id\")\n               .resample(\"1D\").agg(rssi=(\"rssi\", \"median\"),\n                                   b_soc=(\"b_soc\", \"median\")))\n        frames.append(g)\n\n    if not frames:\n        raise ValueError(\"Hiç ölçüm tablosu bulunamadı\")\n    daily = pd.concat(frames, axis=1)\n    daily = daily.dropna(how=\"all\")\n    daily[\"ind_ratio\"] = daily[\"re_l\"] / daily[\"ae\"].where(daily[\"ae\"] > 0)\n    daily[\"cap_ratio\"] = daily[\"re_g\"] / daily[\"ae\"].where(daily[\"ae\"] > 0)\n    return daily.reset_index()\n\n\n# ----------------------------------------------------------------------------\n# 2. ANALİZ — cihaz özetleri, trendler, anomaliler, sağlık skoru\n# ----------------------------------------------------------------------------\ndef _slope_per_day(sub, col):\n    s = sub[[\"day_idx\", col]].dropna()\n    if len(s) < CONFIG[\"min_days_trend\"]:\n        return np.nan\n    x, y = s[\"day_idx\"].values.astype(float), s[col].values.astype(float)\n    if np.ptp(x) < 1:\n        return np.nan\n    return float(np.polyfit(x, y, 1)[0])\n\n\ndef _days_to_limit(current, slope, limit, rising=True):\n    \"\"\"Mevcut değer + eğimle sınıra kaç günde ulaşılır (365 ile sınırlı).\"\"\"\n    if not np.isfinite(current) or not np.isfinite(slope):\n        return None\n    if rising:\n        if current >= limit:\n            return 0\n        if slope <= 1e-12:\n            return None\n        d = (limit - current) / slope\n    else:\n        if current <= limit:\n            return 0\n        if slope >= -1e-12:\n            return None\n        d = (limit - current) / slope\n    return int(d) if 0 <= d <= 365 else None\n\n\ndef analyze(daily, devices_df=None, cfg=CONFIG):\n    \"\"\"Cihaz başına özet, bulgular ve sağlık skoru üretir.\"\"\"\n    try:\n        from sklearn.ensemble import IsolationForest\n        HAS_SK = True\n    except Exception:\n        HAS_SK = False\n\n    names = {}\n    if devices_df is not None:\n        names = (devices_df.dropna(subset=[\"id\"]).set_index(\"id\")[\"name\"]\n                 .fillna(\"\").to_dict())\n\n    results = {}\n    feat_cols = [\"ind_ratio\", \"cap_ratio\", \"pf_loaded\", \"thd_v\", \"thd_i\",\n                 \"v_imb\", \"i_imb\", \"pcb_t_max\", \"fq_dev\"]\n\n    for dev, sub in daily.groupby(\"device_id\"):\n        sub = sub.sort_values(\"device_time\").copy()\n        sub[\"day_idx\"] = (sub[\"device_time\"] - sub[\"device_time\"].min()).dt.days\n        n_days = int(sub[\"ae\"].notna().sum()) if \"ae\" in sub else len(sub)\n        ae_tot = float(np.nansum(sub.get(\"ae\", np.nan)))\n        if n_days < 3 or ae_tot <= 0:\n            continue  # analiz için yetersiz veri\n\n        r = {\"device_id\": dev,\n             \"name\": names.get(dev, \"\") or dev,\n             \"days\": int(len(sub)),\n             \"first\": str(sub[\"device_time\"].min().date()),\n             \"last\": str(sub[\"device_time\"].max().date()),\n             \"ae_total_kwh\": ae_tot / 1000.0,\n             \"re_l_total_kvarh\": float(np.nansum(sub.get(\"re_l\", np.nan))) / 1000.0,\n             \"re_g_total_kvarh\": float(np.nansum(sub.get(\"re_g\", np.nan))) / 1000.0}\n        r[\"ind_ratio\"] = r[\"re_l_total_kvarh\"] / r[\"ae_total_kwh\"] if r[\"ae_total_kwh\"] > 0 else np.nan\n        r[\"cap_ratio\"] = r[\"re_g_total_kvarh\"] / r[\"ae_total_kwh\"] if r[\"ae_total_kwh\"] > 0 else np.nan\n\n        # son 30 gün oranı (güncel durum)\n        last30 = sub[sub[\"device_time\"] >= sub[\"device_time\"].max() - pd.Timedelta(days=30)]\n        ae30 = np.nansum(last30.get(\"ae\", np.nan))\n        r[\"ind_ratio_30g\"] = float(np.nansum(last30.get(\"re_l\", np.nan)) / ae30) if ae30 > 0 else np.nan\n        r[\"cap_ratio_30g\"] = float(np.nansum(last30.get(\"re_g\", np.nan)) / ae30) if ae30 > 0 else np.nan\n\n        for col in [\"pf_loaded\", \"thd_v\", \"thd_i\", \"v_imb\", \"i_imb\", \"pcb_t_max\", \"rssi\"]:\n            r[col] = float(np.nanmedian(sub[col])) if col in sub and sub[col].notna().any() else np.nan\n\n        vrms = float(np.nanmedian(sub[\"vrms_med\"])) if \"vrms_med\" in sub and sub[\"vrms_med\"].notna().any() else np.nan\n        lo, hi = cfg[\"vrms_plausible\"]\n        r[\"calibrated\"] = bool(np.isfinite(vrms) and lo <= vrms <= hi)\n        r[\"vrms_med\"] = vrms\n\n        # trendler (birim/gün)\n        r[\"trend\"] = {c: _slope_per_day(sub, c) for c in\n                      [\"pf_loaded\", \"thd_i\", \"i_imb\", \"ind_ratio\", \"pcb_t_max\"]}\n        # sınır projeksiyonları\n        r[\"proj\"] = {\n            \"ind_ratio\": _days_to_limit(r[\"ind_ratio_30g\"], r[\"trend\"][\"ind_ratio\"], cfg[\"ind_limit\"], rising=True),\n            \"thd_i\": _days_to_limit(r[\"thd_i\"], r[\"trend\"][\"thd_i\"], cfg[\"thd_i_limit\"], rising=True),\n            \"i_imb\": _days_to_limit(r[\"i_imb\"], r[\"trend\"][\"i_imb\"], cfg[\"i_imb_limit\"], rising=True),\n            \"pf_loaded\": _days_to_limit(r[\"pf_loaded\"], r[\"trend\"][\"pf_loaded\"], cfg[\"pf_target\"], rising=False),\n        }\n\n        # anomali tespiti (IsolationForest, gün bazında)\n        r[\"anomaly_ratio\"] = np.nan\n        r[\"anomaly_days\"] = []\n        X = sub[[c for c in feat_cols if c in sub]].copy()\n        X = X.loc[:, X.notna().mean() > 0.5]\n        X = X.dropna()\n        if HAS_SK and len(X) >= cfg[\"min_days_iforest\"] and X.shape[1] >= 3:\n            iso = IsolationForest(contamination=cfg[\"anomaly_contamination\"], random_state=42)\n            lab = iso.fit_predict((X - X.mean()) / X.std(ddof=0).replace(0, 1))\n            anom_idx = X.index[lab == -1]\n            r[\"anomaly_ratio\"] = float(len(anom_idx) / len(X))\n            r[\"anomaly_days\"] = [str(d.date()) for d in\n                                 sub.loc[anom_idx, \"device_time\"].sort_values().tail(5)]\n\n        r[\"findings\"] = _findings(r, cfg)\n        r[\"score\"] = _score(r, cfg)\n        r[\"risk\"] = _risk(r[\"score\"])\n        results[dev] = r\n\n    return results\n\n\ndef _findings(r, cfg):\n    F = []\n\n    def add(code, severity, value, limit, **kw):\n        F.append(dict(code=code, severity=severity, value=value, limit=limit, **kw))\n\n    ir = r.get(\"ind_ratio_30g\")\n    if np.isfinite(ir or np.nan):\n        if ir > cfg[\"ind_limit\"]:\n            add(\"REAKTIF_IND_ASIM\", \"yüksek\" if ir > 1.5 * cfg[\"ind_limit\"] else \"orta\",\n                ir, cfg[\"ind_limit\"])\n        elif ir > 0.8 * cfg[\"ind_limit\"]:\n            add(\"REAKTIF_IND_YAKIN\", \"düşük\", ir, cfg[\"ind_limit\"])\n    cr = r.get(\"cap_ratio_30g\")\n    if np.isfinite(cr or np.nan):\n        if cr > cfg[\"cap_limit\"]:\n            add(\"REAKTIF_CAP_ASIM\", \"yüksek\" if cr > 1.5 * cfg[\"cap_limit\"] else \"orta\",\n                cr, cfg[\"cap_limit\"])\n        elif cr > 0.8 * cfg[\"cap_limit\"]:\n            add(\"REAKTIF_CAP_YAKIN\", \"düşük\", cr, cfg[\"cap_limit\"])\n\n    pf = r.get(\"pf_loaded\")\n    if np.isfinite(pf or np.nan) and pf < cfg[\"pf_target\"]:\n        add(\"PF_DUSUK\", \"yüksek\" if pf < 0.85 else \"orta\", pf, cfg[\"pf_target\"])\n\n    for key, code in [(\"thd_i\", \"THD_AKIM\"), (\"thd_v\", \"THD_GERILIM\")]:\n        val = r.get(key)\n        lim = cfg[key + \"_limit\"]\n        if np.isfinite(val or np.nan) and val > lim:\n            add(code, \"yüksek\" if val > 2 * lim else \"orta\", val, lim)\n\n    for key, code, lim in [(\"i_imb\", \"AKIM_DENGESIZ\", cfg[\"i_imb_limit\"]),\n                           (\"v_imb\", \"GERILIM_DENGESIZ\", cfg[\"v_imb_limit\"])]:\n        val = r.get(key)\n        if r.get(\"calibrated\") is False and code == \"GERILIM_DENGESIZ\":\n            pass  # dengesizlik oransal, kalibrasyonsuz da anlamlı — yine de raporla\n        if np.isfinite(val or np.nan) and val > lim:\n            add(code, \"yüksek\" if val > 2 * lim else \"orta\", val, lim)\n\n    t = r.get(\"pcb_t_max\")\n    if np.isfinite(t or np.nan) and t > cfg[\"pcb_t_limit\"]:\n        add(\"SICAKLIK\", \"orta\", t, cfg[\"pcb_t_limit\"])\n\n    ar = r.get(\"anomaly_ratio\")\n    if np.isfinite(ar or np.nan) and ar > 0.12:\n        add(\"ANOMALI\", \"orta\", ar, 0.12, days=r.get(\"anomaly_days\", []))\n\n    for key, code in [(\"ind_ratio\", \"TREND_REAKTIF\"), (\"thd_i\", \"TREND_THD\"),\n                      (\"i_imb\", \"TREND_DENGESIZ\")]:\n        d = r[\"proj\"].get(key if key != \"ind_ratio\" else \"ind_ratio\")\n        if d is not None and 0 < d <= 90:\n            add(code, \"orta\" if d > 30 else \"yüksek\", d, 90, metric=key)\n    dpf = r[\"proj\"].get(\"pf_loaded\")\n    if dpf is not None and 0 < dpf <= 90:\n        add(\"TREND_PF\", \"orta\" if dpf > 30 else \"yüksek\", dpf, 90, metric=\"pf_loaded\")\n\n    if not r.get(\"calibrated\", True):\n        add(\"KALIBRASYON\", \"bilgi\", r.get(\"vrms_med\"), None)\n    if r[\"days\"] < 14:\n        add(\"VERI_AZ\", \"bilgi\", r[\"days\"], 14)\n    return F\n\n\ndef _score(r, cfg):\n    s = 100.0\n    ir, cr = r.get(\"ind_ratio_30g\"), r.get(\"cap_ratio_30g\")\n    if np.isfinite(ir or np.nan) and ir > cfg[\"ind_limit\"]:\n        s -= min(25, 25 * (ir - cfg[\"ind_limit\"]) / cfg[\"ind_limit\"] + 10)\n    if np.isfinite(cr or np.nan) and cr > cfg[\"cap_limit\"]:\n        s -= min(15, 15 * (cr - cfg[\"cap_limit\"]) / cfg[\"cap_limit\"] + 6)\n    pf = r.get(\"pf_loaded\")\n    if np.isfinite(pf or np.nan) and pf < cfg[\"pf_target\"]:\n        s -= min(15, (cfg[\"pf_target\"] - pf) * 100)\n    if np.isfinite(r.get(\"thd_i\") or np.nan) and r[\"thd_i\"] > cfg[\"thd_i_limit\"]:\n        s -= min(10, 50 * (r[\"thd_i\"] - cfg[\"thd_i_limit\"]))\n    if np.isfinite(r.get(\"thd_v\") or np.nan) and r[\"thd_v\"] > cfg[\"thd_v_limit\"]:\n        s -= min(10, 100 * (r[\"thd_v\"] - cfg[\"thd_v_limit\"]))\n    if np.isfinite(r.get(\"i_imb\") or np.nan) and r[\"i_imb\"] > cfg[\"i_imb_limit\"]:\n        s -= min(10, 50 * (r[\"i_imb\"] - cfg[\"i_imb_limit\"]))\n    if np.isfinite(r.get(\"v_imb\") or np.nan) and r[\"v_imb\"] > cfg[\"v_imb_limit\"]:\n        s -= min(5, 100 * (r[\"v_imb\"] - cfg[\"v_imb_limit\"]))\n    ar = r.get(\"anomaly_ratio\")\n    if np.isfinite(ar or np.nan):\n        s -= min(10, max(0, (ar - 0.08)) * 100)\n    n_trend = sum(1 for f in r[\"findings\"] if f[\"code\"].startswith(\"TREND\"))\n    s -= min(10, 5 * n_trend)\n    return round(max(0.0, s), 1)\n",
   "id": "ydxxzHrpJAhh"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "jd0Of1kPJAhi",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470038518,
     "user_tz": -180,
     "elapsed": 60,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "76c3b458-1124-4d27-893b-f751ca622f42"
   },
   "execution_count": 4,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_narrator.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_narrator.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU Offline Narrator — şablon tabanlı, internet/LLM gerektirmeyen Türkçe rapor üretici.\nDört hedef kitle: teknik uzman, özel sektör yöneticisi, kamu yöneticisi, vatandaş.\n\"\"\"\nimport numpy as np\n\nAUDIENCES = [\"teknik\", \"yonetici_ozel\", \"yonetici_kamu\", \"vatandas\"]\n\nAUDIENCE_META = {\n    \"teknik\": dict(baslik=\"Teknik Uzman Raporu\",\n                   dil=\"mühendislik terminolojisi, sayısal detay, standart referansları\"),\n    \"yonetici_ozel\": dict(baslik=\"Yönetici Özeti (Özel Sektör)\",\n                          dil=\"iş etkisi, maliyet/ceza riski, önceliklendirilmiş aksiyon\"),\n    \"yonetici_kamu\": dict(baslik=\"Yönetici Özeti (Kamu)\",\n                          dil=\"mevzuat uyumu, hizmet sürekliliği, kamu kaynağı verimliliği\"),\n    \"vatandas\": dict(baslik=\"Vatandaş Bilgilendirme Notu\",\n                     dil=\"sade Türkçe, benzetmeli anlatım, teknik terim yok\"),\n}\n\n\n# ---------------------------------------------------------------- yardımcılar\ndef pct(x, nd=1):\n    return \"—\" if x is None or not np.isfinite(x) else f\"%{x*100:.{nd}f}\".replace(\".\", \",\")\n\ndef num(x, nd=2, unit=\"\"):\n    if x is None or not np.isfinite(x):\n        return \"—\"\n    s = f\"{x:,.{nd}f}\".replace(\",\", \"§\").replace(\".\", \",\").replace(\"§\", \".\")\n    return s + ((\" \" + unit) if unit else \"\")\n\ndef _sev_word(sev, aud):\n    tablo = {\n        \"teknik\": {\"yüksek\": \"KRİTİK\", \"orta\": \"UYARI\", \"düşük\": \"İZLEME\", \"bilgi\": \"BİLGİ\"},\n        \"yonetici_ozel\": {\"yüksek\": \"Acil\", \"orta\": \"Önemli\", \"düşük\": \"Takip\", \"bilgi\": \"Not\"},\n        \"yonetici_kamu\": {\"yüksek\": \"Acil\", \"orta\": \"Önemli\", \"düşük\": \"Takip\", \"bilgi\": \"Not\"},\n        \"vatandas\": {\"yüksek\": \"önemli bir sorun\", \"orta\": \"dikkat edilmesi gereken bir durum\",\n                     \"düşük\": \"küçük bir uyarı\", \"bilgi\": \"bilgi notu\"},\n    }\n    return tablo[aud][sev]\n\n\n# ------------------------------------------------- bulgu cümleleri (kitleye göre)\ndef _finding_text(f, r, aud):\n    c, v, lim, sev = f[\"code\"], f.get(\"value\"), f.get(\"limit\"), f[\"severity\"]\n    S = _sev_word(sev, aud)\n\n    if c == \"REAKTIF_IND_ASIM\":\n        t = {\n            \"teknik\": f\"[{S}] Endüktif reaktif/aktif enerji oranı son 30 günde {pct(v)} — EPDK sınırı {pct(lim,0)}'in üzerinde. Kompanzasyon panosu devre dışı ya da kondansatör kademeleri yetersiz/arızalı olabilir; motor yüklerinin cosφ'si düşük.\",\n            \"yonetici_ozel\": f\"[{S}] Bu tesis reaktif enerji sınırını aşıyor (ölçülen {pct(v)}, yasal sınır {pct(lim,0)}). Dağıtım şirketi bu durumda reaktif ceza bedeli tahakkuk ettirir; kompanzasyon yatırımı/bakımı bu cezayı tamamen ortadan kaldırabilir.\",\n            \"yonetici_kamu\": f\"[{S}] Tesis, elektrik mevzuatındaki reaktif enerji sınırını ({pct(lim,0)}) aşmaktadır (ölçülen {pct(v)}). Bu, elektrik faturasına yansıyan ve önlenebilir bir kamu kaynağı kaybıdır; kompanzasyon sisteminin kontrolü gerekir.\",\n            \"vatandas\": f\"Bu tesiste {S} tespit edildi: tesis, şebekeden aldığı elektriğin bir kısmını verimsiz kullanıyor (buna 'reaktif enerji' denir — bardağı doldururken yanına taşan su gibi düşünebilirsiniz). Bu durum faturayı artırır; basit bir elektrik panosu bakımı ile düzeltilebilir.\",\n        }\n    elif c == \"REAKTIF_IND_YAKIN\":\n        t = {\n            \"teknik\": f\"[{S}] Endüktif oran {pct(v)} — sınırın ({pct(lim,0)}) %80'ini geçti. Kademe ayarları gözden geçirilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Reaktif oran {pct(v)} ile ceza sınırına yaklaşıyor; küçük bir ayarla ceza riski önlenebilir.\",\n            \"yonetici_kamu\": f\"[{S}] Reaktif oran {pct(v)}; yasal sınıra yaklaşılmıştır, önleyici kontrol önerilir.\",\n            \"vatandas\": f\"Tesisin elektrik kullanım verimliliği sınıra yaklaşıyor; şimdiden yapılacak küçük bir bakım ileride fatura artışını önler.\",\n        }\n    elif c == \"REAKTIF_CAP_ASIM\":\n        t = {\n            \"teknik\": f\"[{S}] Kapasitif reaktif oran {pct(v)} — EPDK sınırı {pct(lim,0)} aşıldı. Aşırı kompanzasyon: kondansatör kademeleri düşük yükte devrede kalıyor; RG rölesi ayarı/kademe küçültme gerekli.\",\n            \"yonetici_ozel\": f\"[{S}] Tesis şebekeye gereksiz 'kapasitif' enerji veriyor ({pct(v)}, sınır {pct(lim,0)}) — bu da ceza kalemidir. Kompanzasyon ayarı tek seferlik düşük maliyetli bir müdahaledir.\",\n            \"yonetici_kamu\": f\"[{S}] Kapasitif reaktif sınır aşımı ({pct(v)} > {pct(lim,0)}) tespit edildi; fatura cezası riski için kompanzasyon ayarı gereklidir.\",\n            \"vatandas\": f\"Tesisin elektrik düzeltme sistemi gereğinden fazla çalışıyor; bu da tıpkı eksik çalışması gibi faturaya ceza olarak yansıyabilir. Ayar gerektiren bir durumdur.\",\n        }\n    elif c == \"REAKTIF_CAP_YAKIN\":\n        t = {\n            \"teknik\": f\"[{S}] Kapasitif oran {pct(v)}, sınırın %80'inde. Düşük yük saatlerinde kademe çıkışı kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Kapasitif reaktif oran sınıra yaklaşıyor ({pct(v)}); önleyici ayar önerilir.\",\n            \"yonetici_kamu\": f\"[{S}] Kapasitif oran {pct(v)}; sınır aşımı öncesi kontrol önerilir.\",\n            \"vatandas\": f\"Tesisin elektrik sisteminde küçük bir ayar ihtiyacı görünüyor; şu an sorun yok ama takipte.\",\n        }\n    elif c == \"PF_DUSUK\":\n        t = {\n            \"teknik\": f\"[{S}] Yük altındaki ortalama güç faktörü {num(v)} (hedef ≥ {num(lim)}). Düşük cosφ hat kayıplarını ve trafo/kablo ısınmasını artırır; motor verimsizliği veya kompanzasyon eksikliği göstergesidir.\",\n            \"yonetici_ozel\": f\"[{S}] Güç faktörü {num(v)} — hedefin ({num(lim)}) altında. Aynı iş için şebekeden daha fazla akım çekiliyor: kayıplar, ısınma ve ceza riski artıyor.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste güç faktörü {num(v)} ölçülmüştür (hedef {num(lim)}). Enerji verimliliği düşüktür; iyileştirme, işletme giderini kalıcı olarak azaltır.\",\n            \"vatandas\": f\"Tesis, kullandığı elektriği ideal verimle kullanamıyor (verim göstergesi {num(v)}; ideali {num(lim)} ve üzeri). Bakımla düzeltilebilir, hizmetin aksaması söz konusu değil.\",\n        }\n    elif c == \"THD_AKIM\":\n        t = {\n            \"teknik\": f\"[{S}] Akım THD medyanı {pct(v)} (eşik {pct(lim,0)}). Harmonik kaynaklı ek ısınma motor/trafo ömrünü kısaltır; sürücü (VFD) filtreleri ve kondansatör rezonansı kontrol edilmeli (IEEE 519 referansı).\",\n            \"yonetici_ozel\": f\"[{S}] Elektrik akımında yüksek bozulma (harmonik) ölçüldü ({pct(v)}). Bu, ekipman ömrünü kısaltan ve arıza sıklığını artıran sinsi bir maliyet kalemidir; filtre yatırımı değerlendirilmelidir.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste akım harmonik bozulması {pct(v)} seviyesindedir (eşik {pct(lim,0)}). Ekipman ömrü ve arıza riski açısından planlı bakım kapsamına alınmalıdır.\",\n            \"vatandas\": f\"Tesisteki elektrik akımında 'parazit' benzeri bir bozulma ölçüldü. Bu, cihazların zamanla daha çabuk yıpranmasına yol açabilir; teknik ekipler gerekli bakımı planlayabilir.\",\n        }\n    elif c == \"THD_GERILIM\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim THD {pct(v)} (sınır {pct(lim,0)}, IEEE 519). Şebeke tarafı veya tesis içi harmonik kaynakları araştırılmalı.\",\n            \"yonetici_ozel\": f\"[{S}] Şebeke geriliminde bozulma ölçüldü ({pct(v)}); hassas cihazlar için risk oluşturabilir.\",\n            \"yonetici_kamu\": f\"[{S}] Gerilim kalitesinde bozulma ({pct(v)}) tespit edildi; dağıtım şirketi ile birlikte incelenmesi önerilir.\",\n            \"vatandas\": f\"Tesise gelen elektriğin kalitesinde dalgalanma görüldü; takip ediliyor.\",\n        }\n    elif c == \"AKIM_DENGESIZ\":\n        t = {\n            \"teknik\": f\"[{S}] Üç faz akım dengesizliği {pct(v)} (eşik {pct(lim,0)}). Olası nedenler: tek faz yük dağılımı, gevşek klemens, sargı direnç farkı, kondansatör faz arızası. Dengesizlik negatif bileşen ısınması üzerinden motor ömrünü hızla tüketir.\",\n            \"yonetici_ozel\": f\"[{S}] Motorun üç fazı eşit yüklenmiyor (dengesizlik {pct(v)}). Bu, motor arızasının en sık erken habercisidir; planlı kontrol plansız duruştan kat kat ucuzdur.\",\n            \"yonetici_kamu\": f\"[{S}] Tesiste faz dengesizliği {pct(v)} ölçülmüştür. Pompanın/motorun beklenmedik arızasını önlemek için planlı bakım önerilir; hizmet kesintisi riski azaltılır.\",\n            \"vatandas\": f\"Tesisteki motorun bir tarafı diğerlerinden fazla zorlanıyor — üç tekerleği sağlam, biri inik bir araç gibi. Erken bakım yapılırsa büyük arıza ve hizmet kesintisi yaşanmaz.\",\n        }\n    elif c == \"GERILIM_DENGESIZ\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim dengesizliği {pct(v)} (sınır {pct(lim,0)}, IEC/NEMA). %1 gerilim dengesizliği ~%6-10 akım dengesizliğine yol açar; besleme ve bağlantılar kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Besleme geriliminde dengesizlik ({pct(v)}) var; motor ısınması ve verim kaybına yol açar.\",\n            \"yonetici_kamu\": f\"[{S}] Besleme gerilimi dengesizliği ({pct(v)}) tespit edildi; şebeke bağlantısı kontrol edilmelidir.\",\n            \"vatandas\": f\"Tesise gelen elektrik hatlarında küçük bir dengesizlik ölçüldü; teknik ekip takibinde.\",\n        }\n    elif c == \"SICAKLIK\":\n        t = {\n            \"teknik\": f\"[{S}] Pano/elektronik sıcaklığı {num(v,1,'°C')} (eşik {num(lim,0,'°C')}). Havalandırma, toz ve bağlantı temasları kontrol edilmeli.\",\n            \"yonetici_ozel\": f\"[{S}] Kontrol panosunda yüksek sıcaklık ({num(v,1,'°C')}); elektronik ömrü ve yangın riski açısından kontrol edilmeli.\",\n            \"yonetici_kamu\": f\"[{S}] Panoda yüksek sıcaklık ({num(v,1,'°C')}) ölçüldü; iş güvenliği ve ekipman ömrü için kontrol planlanmalıdır.\",\n            \"vatandas\": f\"Cihaz dolabında normalden yüksek sıcaklık ölçüldü; kontrol planlandı.\",\n        }\n    elif c == \"ANOMALI\":\n        days = \", \".join(f.get(\"days\", [])[-3:])\n        t = {\n            \"teknik\": f\"[{S}] Makine öğrenmesi (IsolationForest) günlerin {pct(v)}'ini anormal işaretledi (son örnekler: {days}). İşletme kayıtlarıyla karşılaştırılmalı.\",\n            \"yonetici_ozel\": f\"[{S}] Otomatik analiz, çalışma günlerinin {pct(v)}'inde olağan dışı davranış tespit etti; teknik inceleme önerilir.\",\n            \"yonetici_kamu\": f\"[{S}] Otomatik izleme sistemi olağan dışı çalışma günleri tespit etti ({pct(v)}); teknik birim bilgilendirilmelidir.\",\n            \"vatandas\": f\"Otomatik izleme sistemi bazı günlerde alışılmadık çalışma fark etti; uzmanlar inceliyor.\",\n        }\n    elif c in (\"TREND_REAKTIF\", \"TREND_THD\", \"TREND_DENGESIZ\", \"TREND_PF\"):\n        metric_ad = {\"TREND_REAKTIF\": \"reaktif oran\", \"TREND_THD\": \"akım harmoniği\",\n                     \"TREND_DENGESIZ\": \"faz dengesizliği\", \"TREND_PF\": \"güç faktörü\"}[c]\n        t = {\n            \"teknik\": f\"[{S}] Kestirim: mevcut eğilim sürerse {metric_ad} ~{int(v)} gün içinde sınırı aşacak. Kök neden analizi için pencere: önümüzdeki {max(7,int(v)//2)} gün.\",\n            \"yonetici_ozel\": f\"[{S}] Öngörü: {metric_ad} kötüleşiyor; yaklaşık {int(v)} gün içinde sınır aşımı bekleniyor. Şimdi planlanan bakım, ceza/arıza gerçekleşmeden sorunu çözer.\",\n            \"yonetici_kamu\": f\"[{S}] Öngörü: {metric_ad} göstergesi yaklaşık {int(v)} gün içinde sınırı aşabilir; bütçeli planlı bakım için zaman penceresi mevcuttur.\",\n            \"vatandas\": f\"Ölçümler, yaklaşık {int(v)} gün içinde bakım gerekebileceğini gösteriyor. Sorun büyümeden planlama yapılıyor — hizmette kesinti beklenmiyor.\",\n        }\n    elif c == \"KALIBRASYON\":\n        t = {\n            \"teknik\": f\"[{S}] Gerilim ölçümleri kalibre edilmemiş görünüyor (medyan VRMS {num(v,0)}). Mutlak değerler yerine oransal metrikler (dengesizlik, THD, PF, reaktif oran) esas alındı; VT/CT kalibrasyonu önerilir.\",\n            \"yonetici_ozel\": f\"[{S}] Bu cihazın ölçüm kalibrasyonu tamamlanmamış; oransal göstergeler güvenilir, mutlak kW/V değerleri kalibrasyon sonrası netleşecek.\",\n            \"yonetici_kamu\": f\"[{S}] Cihaz kalibrasyonu beklemede; raporda oransal göstergeler esas alınmıştır.\",\n            \"vatandas\": f\"Bu noktadaki ölçüm cihazının ince ayarı henüz tamamlanmadı; genel değerlendirme yine de güvenilirdir.\",\n        }\n    elif c == \"VERI_AZ\":\n        t = {\n            \"teknik\": f\"[{S}] Yalnızca {int(v)} günlük veri mevcut (önerilen ≥ {int(lim)}); trend ve anomali kestirimlerinin güven aralığı geniştir.\",\n            \"yonetici_ozel\": f\"[{S}] Veri geçmişi kısa ({int(v)} gün); sonuçlar ilk izlenimdir, izleme sürüyor.\",\n            \"yonetici_kamu\": f\"[{S}] İzleme yeni başlamıştır ({int(v)} gün); değerlendirme güncellenecektir.\",\n            \"vatandas\": f\"Bu nokta yakın zamanda izlemeye alındı; değerlendirme her gün biraz daha kesinleşiyor.\",\n        }\n    else:\n        t = {a: f\"[{S}] {c}: {v}\" for a in AUDIENCES}\n    return t[aud]\n\n\n# ------------------------------------------------------------- öneri motoru\n_RECS = {\n    \"REAKTIF_IND_ASIM\": {\n        \"teknik\": \"Kompanzasyon panosu: RG rölesi ayarları, kondansatör kapasite ölçümü, kontaktör/sigorta kontrolü; gerekirse kademe ekleme.\",\n        \"mgr\": \"Kompanzasyon bakımı/yatırımı için teknik ekibe iş emri açılması (tipik geri ödeme süresi aylar mertebesinde).\",\n        \"vatandas\": \"Elektrik panosunda planlı bakım yapılacak.\",\n    },\n    \"REAKTIF_CAP_ASIM\": {\n        \"teknik\": \"Düşük yükte kademe çıkışını sağlayacak RG röle ayarı; en küçük kademenin küçültülmesi veya şönt reaktör değerlendirmesi.\",\n        \"mgr\": \"Kompanzasyon ayarı için kısa süreli teknik müdahale planlanması.\",\n        \"vatandas\": \"Sistem ayarı yapılacak.\",\n    },\n    \"PF_DUSUK\": {\n        \"teknik\": \"Motor yüklenme analizi (boşta/az yükte çalışmayı azalt), kompanzasyon kademe yeterliliği, gerekiyorsa VFD parametre kontrolü.\",\n        \"mgr\": \"Verimlilik iyileştirmesi: pompa çalışma programı ve kompanzasyon birlikte gözden geçirilmeli.\",\n        \"vatandas\": \"Verimlilik bakımı planlanacak.\",\n    },\n    \"THD_AKIM\": {\n        \"teknik\": \"Harmonik kaynak taraması (VFD, doğrultucular); pasif/aktif filtre; kondansatörlerde detuned reaktör kontrolü.\",\n        \"mgr\": \"Harmonik filtre yatırımı için teknik fizibilite talep edilmeli.\",\n        \"vatandas\": \"Elektrik kalitesi için ek donanım değerlendirilecek.\",\n    },\n    \"AKIM_DENGESIZ\": {\n        \"teknik\": \"Termal kamera ile klemens taraması, faz akım ölçüm doğrulaması, motor sargı direnç/izolasyon testi (megger), kondansatör faz kontrolü.\",\n        \"mgr\": \"Planlı motor/pano kontrolü — plansız duruş riskine karşı en yüksek öncelik.\",\n        \"vatandas\": \"Motor kontrol bakımı planlanacak.\",\n    },\n    \"GERILIM_DENGESIZ\": {\n        \"teknik\": \"Besleme bağlantıları ve şebeke tarafı ölçüm; dağıtım şirketi ile gerilim kalitesi kaydı paylaşımı.\",\n        \"mgr\": \"Dağıtım şirketi nezdinde gerilim kalitesi başvurusu değerlendirilmeli.\",\n        \"vatandas\": \"Elektrik şirketi ile birlikte kontrol yapılacak.\",\n    },\n    \"SICAKLIK\": {\n        \"teknik\": \"Pano havalandırma/filtre temizliği, termal tarama, bağlantı tork kontrolü.\",\n        \"mgr\": \"Pano bakımı iş emri.\",\n        \"vatandas\": \"Dolap bakımı yapılacak.\",\n    },\n}\n\n\ndef _recs_for(findings, aud):\n    out, seen = [], set()\n    key = \"teknik\" if aud == \"teknik\" else (\"vatandas\" if aud == \"vatandas\" else \"mgr\")\n    for f in sorted(findings, key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n        base = f[\"code\"].replace(\"_YAKIN\", \"_ASIM\").replace(\"TREND_REAKTIF\", \"REAKTIF_IND_ASIM\") \\\n                        .replace(\"TREND_THD\", \"THD_AKIM\").replace(\"TREND_DENGESIZ\", \"AKIM_DENGESIZ\") \\\n                        .replace(\"TREND_PF\", \"PF_DUSUK\")\n        rec = _RECS.get(base, {}).get(key)\n        if rec and base not in seen:\n            out.append(rec)\n            seen.add(base)\n    return out\n\n\n# ------------------------------------------------------------ cihaz raporu\ndef device_report(r, aud):\n    meta = AUDIENCE_META[aud]\n    L = []\n    L.append(f\"## {r['name']} — {meta['baslik']}\")\n    L.append(f\"*Dönem: {r['first']} → {r['last']} ({r['days']} gün) · Rapor: otomatik üretim (offline narrator)*\\n\")\n\n    risk_tr = {\"İYİ\": \"iyi durumda\", \"İZLENMELİ\": \"izlenmesi gereken\",\n               \"RİSKLİ\": \"riskli\", \"KRİTİK\": \"kritik\"}\n\n    if aud == \"teknik\":\n        L.append(f\"**Sağlık skoru: {r['score']}/100 ({r['risk']})**\\n\")\n        L.append(\"| Metrik | Değer | Eşik |\")\n        L.append(\"|---|---|---|\")\n        L.append(f\"| Aktif enerji (dönem) | {num(r['ae_total_kwh'],1,'kWh')} | — |\")\n        L.append(f\"| Endüktif oran (30g) | {pct(r.get('ind_ratio_30g'))} | %20 (EPDK) |\")\n        L.append(f\"| Kapasitif oran (30g) | {pct(r.get('cap_ratio_30g'))} | %15 (EPDK) |\")\n        L.append(f\"| Güç faktörü (yükte) | {num(r.get('pf_loaded'))} | ≥0,95 |\")\n        L.append(f\"| THD-I / THD-V (medyan) | {pct(r.get('thd_i'))} / {pct(r.get('thd_v'))} | %15 / %8 |\")\n        L.append(f\"| Akım / gerilim dengesizliği | {pct(r.get('i_imb'))} / {pct(r.get('v_imb'))} | %10 / %2 |\")\n        if np.isfinite(r.get(\"anomaly_ratio\") or np.nan):\n            L.append(f\"| Anomali oranı (IForest) | {pct(r['anomaly_ratio'])} | %12 |\")\n        L.append(\"\")\n    elif aud == \"vatandas\":\n        durum = risk_tr[r[\"risk\"]]\n        L.append(f\"Bu tesis şu anda **{durum}** olarak değerlendiriliyor \"\n                 f\"(100 üzerinden {int(r['score'])} puan). Değerlendirme, tesisteki elektrik \"\n                 f\"ölçüm cihazının {r['days']} günlük verisine dayanıyor.\\n\")\n    else:\n        kamu = \" Kamu hizmet sürekliliği önceliklidir.\" if aud == \"yonetici_kamu\" else \"\"\n        L.append(f\"**Durum: {r['risk']} (skor {r['score']}/100)** — dönem tüketimi \"\n                 f\"{num(r['ae_total_kwh'],0,'kWh')}.{kamu}\\n\")\n\n    if r[\"findings\"]:\n        L.append(\"**Bulgular**\" if aud != \"vatandas\" else \"**Tespit edilenler**\")\n        for f in sorted(r[\"findings\"], key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n            L.append(f\"- {_finding_text(f, r, aud)}\")\n        L.append(\"\")\n    else:\n        msg = {\"teknik\": \"Tüm metrikler eşik değerlerin içinde; rutin izleme sürdürülmeli.\",\n               \"vatandas\": \"Her şey yolunda görünüyor; düzenli izleme devam ediyor.\"}\n        L.append(msg.get(aud, \"Önemli bir bulgu yok; rutin izleme sürüyor.\") + \"\\n\")\n\n    recs = _recs_for(r[\"findings\"], aud)\n    if recs:\n        L.append(\"**Önerilen aksiyonlar**\" if aud != \"vatandas\" else \"**Ne yapılacak?**\")\n        for i, rec in enumerate(recs, 1):\n            L.append(f\"{i}. {rec}\")\n        L.append(\"\")\n    return \"\\n\".join(L)\n\n\n# -------------------------------------------------------------- filo raporu\ndef fleet_report(results, aud):\n    meta = AUDIENCE_META[aud]\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    n = len(rs)\n    krit = [r for r in rs if r[\"risk\"] == \"KRİTİK\"]\n    risk = [r for r in rs if r[\"risk\"] == \"RİSKLİ\"]\n    izle = [r for r in rs if r[\"risk\"] == \"İZLENMELİ\"]\n    iyi = [r for r in rs if r[\"risk\"] == \"İYİ\"]\n    toplam_kwh = sum(r[\"ae_total_kwh\"] for r in rs)\n    reaktif_asim = [r for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                         for f in r[\"findings\"])]\n    L = [f\"# Filo Genel Durum — {meta['baslik']}\",\n         f\"*{n} cihaz · toplam {num(toplam_kwh,0,'kWh')} aktif enerji · offline narrator ile otomatik üretildi*\\n\"]\n\n    if aud == \"vatandas\":\n        L.append(f\"İzlenen **{n} tesisin** {len(iyi)} tanesi iyi durumda, \"\n                 f\"{len(izle)} tanesi yakından izleniyor, {len(risk)+len(krit)} tanesinde \"\n                 f\"bakım planlaması yapılıyor. Amaç: arızaları olmadan önce yakalamak, \"\n                 f\"kesintisiz hizmet ve gereksiz masrafların önlenmesi.\\n\")\n        if risk or krit:\n            L.append(\"Bakım planlanan tesisler: \" + \", \".join(r[\"name\"] for r in (krit + risk)) + \".\\n\")\n    else:\n        L.append(f\"**Dağılım:** KRİTİK {len(krit)} · RİSKLİ {len(risk)} · İZLENMELİ {len(izle)} · İYİ {len(iyi)}\\n\")\n        if reaktif_asim:\n            if aud == \"teknik\":\n                L.append(f\"**Reaktif sınır aşımı olan {len(reaktif_asim)} tesis:** \" +\n                         \", \".join(f\"{r['name']} (end. {pct(r.get('ind_ratio_30g'))}, kap. {pct(r.get('cap_ratio_30g'))})\"\n                                   for r in reaktif_asim) + \"\\n\")\n            else:\n                L.append(f\"**Reaktif ceza riski taşıyan {len(reaktif_asim)} tesis var** — bu, doğrudan \"\n                         f\"faturaya yansıyan ve kompanzasyon bakımı ile tamamen önlenebilir bir kalemdir: \" +\n                         \", \".join(r[\"name\"] for r in reaktif_asim) + \"\\n\")\n        L.append(\"| Tesis | Skor | Durum | End. oran | PF | Öncelikli bulgu |\")\n        L.append(\"|---|---|---|---|---|---|\")\n        for r in rs:\n            top = r[\"findings\"][0][\"code\"] if r[\"findings\"] else \"—\"\n            L.append(f\"| {r['name']} | {r['score']} | {r['risk']} | \"\n                     f\"{pct(r.get('ind_ratio_30g'))} | {num(r.get('pf_loaded'))} | {top} |\")\n        L.append(\"\")\n        if aud == \"teknik\":\n            L.append(\"Eşikler: EPDK reaktif sınırları (end. %20 / kap. %15), IEEE 519 THD, \"\n                     \"IEC/NEMA gerilim dengesizliği %2. Anomali: IsolationForest, gün bazlı özellik uzayı.\\n\")\n        else:\n            oncelik = krit + risk\n            if oncelik:\n                L.append(\"**Önerilen öncelik sırası:** \" +\n                         \" → \".join(f\"{i+1}) {r['name']}\" for i, r in enumerate(oncelik[:5])) + \"\\n\")\n    return \"\\n\".join(L)\n",
   "id": "jd0Of1kPJAhi"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "yhNXMInZJAhj",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470047654,
     "user_tz": -180,
     "elapsed": 5,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "166dede2-7ac8-4b00-b7f0-cb2a9679633d"
   },
   "execution_count": 5,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_report.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_report.py\n# -*- coding: utf-8 -*-\n\"\"\"Grafik üretimi + rapor dosyaları (MD/HTML) + paketleme.\"\"\"\nimport os, base64, html, zipfile\nimport numpy as np\nimport matplotlib\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\n\nimport qapu_narrator as nar\nfrom qapu_pm import CONFIG\n\nplt.rcParams.update({\"figure.dpi\": 110, \"font.size\": 9, \"axes.grid\": True,\n                     \"grid.alpha\": 0.3, \"axes.spines.top\": False, \"axes.spines.right\": False})\n\nRISK_COLORS = {\"İYİ\": \"#2e7d32\", \"İZLENMELİ\": \"#f9a825\", \"RİSKLİ\": \"#ef6c00\", \"KRİTİK\": \"#c62828\"}\n\n\ndef fleet_chart(results, path):\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    fig, ax = plt.subplots(figsize=(8, max(3, 0.35 * len(rs))))\n    names = [r[\"name\"][:28] for r in rs]\n    scores = [r[\"score\"] for r in rs]\n    colors = [RISK_COLORS[r[\"risk\"]] for r in rs]\n    ax.barh(names, scores, color=colors)\n    for y, s in enumerate(scores):\n        ax.text(s + 1, y, f\"{s:.0f}\", va=\"center\", fontsize=8)\n    ax.axvline(70, color=\"#999\", ls=\"--\", lw=1)\n    ax.axvline(85, color=\"#999\", ls=\":\", lw=1)\n    ax.set_xlim(0, 108)\n    ax.set_xlabel(\"Sağlık skoru (0–100)\")\n    ax.set_title(\"Filo Sağlık Durumu — cihaz bazında\")\n    fig.tight_layout()\n    fig.savefig(path)\n    plt.close(fig)\n\n\ndef device_chart(daily, dev_id, r, path, cfg=CONFIG):\n    sub = daily[daily[\"device_id\"] == dev_id].sort_values(\"device_time\")\n    if len(sub) < 2:\n        return False\n    t = sub[\"device_time\"]\n    fig, axes = plt.subplots(2, 2, figsize=(10, 6), sharex=True)\n    fig.suptitle(f\"{r['name']} — günlük göstergeler\", fontsize=11)\n\n    ax = axes[0, 0]\n    ax.plot(t, sub[\"ind_ratio\"] * 100, \"o-\", ms=3, color=\"#c62828\", label=\"Endüktif\")\n    ax.plot(t, sub[\"cap_ratio\"] * 100, \"s-\", ms=3, color=\"#1565c0\", label=\"Kapasitif\")\n    ax.axhline(cfg[\"ind_limit\"] * 100, color=\"#c62828\", ls=\"--\", lw=1)\n    ax.axhline(cfg[\"cap_limit\"] * 100, color=\"#1565c0\", ls=\":\", lw=1)\n    ax.set_title(\"Reaktif/Aktif oran (%) ve EPDK sınırları\")\n    ax.legend(fontsize=7)\n\n    ax = axes[0, 1]\n    if \"pf_loaded\" in sub:\n        ax.plot(t, sub[\"pf_loaded\"], \"o-\", ms=3, color=\"#2e7d32\")\n    ax.axhline(cfg[\"pf_target\"], color=\"#999\", ls=\"--\", lw=1)\n    ax.set_ylim(0, 1.05)\n    ax.set_title(\"Güç faktörü (yük altında)\")\n\n    ax = axes[1, 0]\n    if \"thd_i\" in sub:\n        ax.plot(t, sub[\"thd_i\"] * 100, \"o-\", ms=3, color=\"#6a1b9a\", label=\"THD-I\")\n    if \"thd_v\" in sub:\n        ax.plot(t, sub[\"thd_v\"] * 100, \"s-\", ms=3, color=\"#00838f\", label=\"THD-V\")\n    ax.axhline(cfg[\"thd_i_limit\"] * 100, color=\"#6a1b9a\", ls=\"--\", lw=1)\n    ax.set_title(\"Harmonik bozulma (%)\")\n    ax.legend(fontsize=7)\n\n    ax = axes[1, 1]\n    if \"i_imb\" in sub:\n        ax.plot(t, sub[\"i_imb\"] * 100, \"o-\", ms=3, color=\"#ef6c00\", label=\"Akım\")\n    if \"v_imb\" in sub:\n        ax.plot(t, sub[\"v_imb\"] * 100, \"s-\", ms=3, color=\"#455a64\", label=\"Gerilim\")\n    ax.axhline(cfg[\"i_imb_limit\"] * 100, color=\"#ef6c00\", ls=\"--\", lw=1)\n    ax.set_title(\"Faz dengesizliği (%)\")\n    ax.legend(fontsize=7)\n\n    for ax in axes.flat:\n        ax.tick_params(axis=\"x\", rotation=30)\n    fig.tight_layout()\n    fig.savefig(path)\n    plt.close(fig)\n    return True\n\n\n# ------------------------------------------------- basit MD → HTML çevirici\ndef _md_to_html(md):\n    try:\n        import markdown\n        return markdown.markdown(md, extensions=[\"tables\"])\n    except Exception:\n        return \"<pre style='white-space:pre-wrap'>\" + html.escape(md) + \"</pre>\"\n\n\n_HTML_TMPL = \"\"\"<!doctype html><html lang=\"tr\"><head><meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>{title}</title>\n<style>\n body{{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;max-width:860px;margin:2rem auto;\n      padding:0 16px;color:#222;line-height:1.55}}\n table{{border-collapse:collapse;width:100%;font-size:.92em}}\n th,td{{border:1px solid #ddd;padding:5px 8px;text-align:left}}\n th{{background:#f4f4f4}} img{{max-width:100%}}\n h1,h2{{border-bottom:2px solid #eee;padding-bottom:4px}}\n .foot{{color:#888;font-size:.8em;margin-top:2rem}}\n</style></head><body>{body}\n<p class=\"foot\">Bu rapor, enerji analizörü verilerinden offline narrator ile tamamen\nçevrimdışı/deterministik olarak üretilmiştir (LLM kullanılmamıştır).</p></body></html>\"\"\"\n\n\ndef _embed_img(path):\n    with open(path, \"rb\") as fh:\n        b64 = base64.b64encode(fh.read()).decode()\n    return f'<img src=\"data:image/png;base64,{b64}\" alt=\"grafik\">'\n\n\ndef generate_reports(results, daily, outdir=\"raporlar\", audiences=None, top_device_charts=8):\n    \"\"\"Tüm kitleler için filo + cihaz raporlarını MD ve HTML olarak üretir; zip döner.\"\"\"\n    audiences = audiences or nar.AUDIENCES\n    os.makedirs(outdir, exist_ok=True)\n    chart_dir = os.path.join(outdir, \"grafikler\")\n    os.makedirs(chart_dir, exist_ok=True)\n\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    fleet_chart(results, fleet_png)\n\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    dev_pngs = {}\n    for r in rs_sorted[:top_device_charts]:\n        p = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        if device_chart(daily, r[\"device_id\"], r, p):\n            dev_pngs[r[\"device_id\"]] = p\n\n    written = []\n    for aud in audiences:\n        md = [nar.fleet_report(results, aud)]\n        html_parts = [_md_to_html(md[0]), _embed_img(fleet_png)]\n        for r in rs_sorted:\n            sec = nar.device_report(r, aud)\n            md.append(\"\\n---\\n\\n\" + sec)\n            html_parts.append(\"<hr>\" + _md_to_html(sec))\n            if r[\"device_id\"] in dev_pngs and aud != \"vatandas\":\n                html_parts.append(_embed_img(dev_pngs[r[\"device_id\"]]))\n        base = os.path.join(outdir, f\"rapor_{aud}\")\n        with open(base + \".md\", \"w\") as fh:\n            fh.write(\"\\n\".join(md))\n        title = nar.AUDIENCE_META[aud][\"baslik\"]\n        with open(base + \".html\", \"w\") as fh:\n            fh.write(_HTML_TMPL.format(title=title, body=\"\\n\".join(html_parts)))\n        written += [base + \".md\", base + \".html\"]\n\n    zpath = os.path.join(outdir, \"qapu_raporlar.zip\")\n    with zipfile.ZipFile(zpath, \"w\", zipfile.ZIP_DEFLATED) as z:\n        for p in written + [fleet_png] + list(dev_pngs.values()):\n            z.write(p, os.path.relpath(p, outdir))\n    return zpath, written\n",
   "id": "yhNXMInZJAhj"
  },
  {
   "cell_type": "code",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "Azo7btabJAhk",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1793470055216,
     "user_tz": -180,
     "elapsed": 38,
     "user": {
      "displayName": "Joseph Volkan ÖZCAN",
      "userId": "11389530967736397480"
     }
    },
    "outputId": "ccc0d667-1e23-48e4-8590-672e0bd5beeb"
   },
   "execution_count": 6,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Writing qapu_docx.py\n"
     ]
    }
   ],
   "source": "%%writefile qapu_docx.py\n# -*- coding: utf-8 -*-\n\"\"\"\nQAPU — Profesyonel Word (.docx) rapor üretici\nKapak sayfası, KPI özeti, kurumsal renkli tablolar, risk rozetleri,\ngömülü grafikler ve sayfa numaralı alt bilgi ile.\n\"\"\"\nimport os\nimport numpy as np\nfrom docx import Document\nfrom docx.shared import Pt, Cm, RGBColor\nfrom docx.enum.text import WD_ALIGN_PARAGRAPH\nfrom docx.enum.table import WD_TABLE_ALIGNMENT\nfrom docx.oxml.ns import qn\nfrom docx.oxml import OxmlElement\n\nimport qapu_narrator as nar\nimport qapu_report as rep\n\n# ----------------------------------------------------------- kurumsal palet\nNAVY = RGBColor(0x1F, 0x38, 0x64)\nBLUE = RGBColor(0x2E, 0x74, 0xB5)\nGRAY = RGBColor(0x6B, 0x6B, 0x6B)\nLIGHT = \"F2F5FA\"           # zebra satır dolgusu (hex, # yok)\nRISK_HEX = {\"İYİ\": \"2E7D32\", \"İZLENMELİ\": \"F9A825\", \"RİSKLİ\": \"EF6C00\", \"KRİTİK\": \"C62828\"}\nSEV_HEX = {\"yüksek\": \"C62828\", \"orta\": \"EF6C00\", \"düşük\": \"F9A825\", \"bilgi\": \"6B6B6B\"}\n\n\n# ------------------------------------------------------------- xml yardımcıları\ndef _shade(cell, hexcolor):\n    tcPr = cell._tc.get_or_add_tcPr()\n    shd = OxmlElement(\"w:shd\")\n    shd.set(qn(\"w:val\"), \"clear\")\n    shd.set(qn(\"w:fill\"), hexcolor)\n    tcPr.append(shd)\n\n\ndef _cell_text(cell, text, bold=False, color=None, size=9, align=None):\n    p = cell.paragraphs[0]\n    p.text = \"\"\n    r = p.add_run(text)\n    r.bold = bold\n    r.font.size = Pt(size)\n    if color:\n        r.font.color.rgb = color\n    if align:\n        p.alignment = align\n    return p\n\n\ndef _page_number_footer(section, left_text):\n    foot = section.footer\n    p = foot.paragraphs[0]\n    p.text = \"\"\n    r = p.add_run(left_text + \"   ·   Sayfa \")\n    r.font.size = Pt(8)\n    r.font.color.rgb = GRAY\n    fld = OxmlElement(\"w:fldSimple\")\n    fld.set(qn(\"w:instr\"), \"PAGE\")\n    rpr = OxmlElement(\"w:rPr\")\n    sz = OxmlElement(\"w:sz\"); sz.set(qn(\"w:val\"), \"16\")\n    rpr.append(sz)\n    fld.append(rpr)\n    p._p.append(fld)\n\n\ndef _hr(par):\n    pPr = par._p.get_or_add_pPr()\n    pbdr = OxmlElement(\"w:pBdr\")\n    b = OxmlElement(\"w:bottom\")\n    b.set(qn(\"w:val\"), \"single\"); b.set(qn(\"w:sz\"), \"8\")\n    b.set(qn(\"w:space\"), \"1\"); b.set(qn(\"w:color\"), \"1F3864\")\n    pbdr.append(b)\n    pPr.append(pbdr)\n\n\ndef _styled_table(doc, header, rows, widths_cm, risk_col=None, zebra=True):\n    t = doc.add_table(rows=1 + len(rows), cols=len(header))\n    t.style = \"Table Grid\"\n    t.alignment = WD_TABLE_ALIGNMENT.CENTER\n    for j, h in enumerate(header):\n        c = t.rows[0].cells[j]\n        _cell_text(c, h, bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=9)\n        _shade(c, \"1F3864\")\n    for i, row in enumerate(rows, start=1):\n        for j, val in enumerate(row):\n            c = t.rows[i].cells[j]\n            _cell_text(c, str(val), size=9)\n            if zebra and i % 2 == 0:\n                _shade(c, LIGHT)\n            if risk_col is not None and j == risk_col and str(val) in RISK_HEX:\n                _shade(c, RISK_HEX[str(val)])\n                _cell_text(c, str(val), bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=9,\n                           align=WD_ALIGN_PARAGRAPH.CENTER)\n    for j, w in enumerate(widths_cm):\n        for row in t.rows:\n            row.cells[j].width = Cm(w)\n    return t\n\n\ndef _heading(doc, text, level=1, color=NAVY, rule=False):\n    h = doc.add_heading(\"\", level=level)\n    r = h.add_run(text)\n    r.font.color.rgb = color\n    if rule:\n        _hr(h)\n    return h\n\n\n# --------------------------------------------------------------- kapak sayfası\ndef _cover(doc, aud, results):\n    meta = nar.AUDIENCE_META[aud]\n    rs = list(results.values())\n    first = min(r[\"first\"] for r in rs)\n    last = max(r[\"last\"] for r in rs)\n    krit = sum(1 for r in rs if r[\"risk\"] == \"KRİTİK\")\n    risk = sum(1 for r in rs if r[\"risk\"] == \"RİSKLİ\")\n    reaktif = sum(1 for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                       for f in r[\"findings\"]))\n    kwh = sum(r[\"ae_total_kwh\"] for r in rs)\n\n    for _ in range(5):\n        doc.add_paragraph()\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"QAPU ENERJİ İZLEME\")\n    r.font.size = Pt(14); r.font.color.rgb = BLUE; r.bold = True\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"Reaktif Enerji ve Kestirimci Bakım Raporu\")\n    r.font.size = Pt(26); r.font.color.rgb = NAVY; r.bold = True\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(meta[\"baslik\"])\n    r.font.size = Pt(15); r.font.color.rgb = GRAY\n\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(f\"Veri dönemi: {first} – {last}\")\n    r.font.size = Pt(11); r.font.color.rgb = GRAY\n\n    doc.add_paragraph(); doc.add_paragraph()\n\n    # KPI şeridi\n    kpis = [(f\"{len(rs)}\", \"İzlenen Tesis\"),\n            (f\"{nar.num(kwh,0)}\", \"Toplam kWh\"),\n            (f\"{krit + risk}\", \"Kritik/Riskli Tesis\"),\n            (f\"{reaktif}\", \"Reaktif Sınır Aşımı\")]\n    t = doc.add_table(rows=2, cols=4)\n    t.alignment = WD_TABLE_ALIGNMENT.CENTER\n    for j, (val, lab) in enumerate(kpis):\n        c = t.rows[0].cells[j]\n        _cell_text(c, val, bold=True, color=RGBColor(0xFF, 0xFF, 0xFF), size=20,\n                   align=WD_ALIGN_PARAGRAPH.CENTER)\n        _shade(c, \"1F3864\" if j % 2 == 0 else \"2E74B5\")\n        c2 = t.rows[1].cells[j]\n        _cell_text(c2, lab, size=9, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER)\n        _shade(c2, \"F2F5FA\")\n        for row in t.rows:\n            row.cells[j].width = Cm(4.1)\n\n    for _ in range(6):\n        doc.add_paragraph()\n    p = doc.add_paragraph()\n    p.alignment = WD_ALIGN_PARAGRAPH.CENTER\n    r = p.add_run(\"Bu rapor, enerji analizörü verilerinden offline narrator ile tamamen \"\n                  \"çevrimdışı ve deterministik olarak üretilmiştir (LLM kullanılmamıştır).\")\n    r.font.size = Pt(8); r.italic = True; r.font.color.rgb = GRAY\n    doc.add_page_break()\n\n\n# -------------------------------------------------------------- filo bölümü\ndef _fleet_section(doc, results, aud, fleet_png):\n    rs = sorted(results.values(), key=lambda r: r[\"score\"])\n    _heading(doc, \"1. Filo Genel Durumu\", level=1, rule=True)\n\n    krit = [r for r in rs if r[\"risk\"] == \"KRİTİK\"]\n    risk = [r for r in rs if r[\"risk\"] == \"RİSKLİ\"]\n    izle = [r for r in rs if r[\"risk\"] == \"İZLENMELİ\"]\n    iyi = [r for r in rs if r[\"risk\"] == \"İYİ\"]\n\n    if aud == \"vatandas\":\n        p = doc.add_paragraph()\n        p.add_run(f\"İzlenen {len(rs)} tesisin {len(iyi)} tanesi iyi durumda, {len(izle)} tanesi \"\n                  f\"yakından izleniyor, {len(risk)+len(krit)} tanesinde bakım planlaması yapılıyor. \"\n                  \"Amaç: arızaları olmadan önce yakalamak, kesintisiz hizmet ve gereksiz \"\n                  \"masrafların önlenmesi.\")\n    else:\n        p = doc.add_paragraph()\n        for lab, grp, hx in [(\"KRİTİK\", krit, \"C62828\"), (\"RİSKLİ\", risk, \"EF6C00\"),\n                             (\"İZLENMELİ\", izle, \"F9A825\"), (\"İYİ\", iyi, \"2E7D32\")]:\n            r = p.add_run(f\" ● {lab}: {len(grp)}   \")\n            r.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n            r.bold = True\n\n    if os.path.exists(fleet_png):\n        doc.add_picture(fleet_png, width=Cm(16.5))\n\n    reaktif = [r for r in rs if any(f[\"code\"].startswith(\"REAKTIF\") and f[\"code\"].endswith(\"ASIM\")\n                                    for f in r[\"findings\"])]\n    if reaktif and aud != \"vatandas\":\n        p = doc.add_paragraph()\n        msg = (f\"Reaktif sınır aşımı olan {len(reaktif)} tesis: \" if aud == \"teknik\" else\n               f\"Reaktif ceza riski taşıyan {len(reaktif)} tesis var — kompanzasyon bakımı ile \"\n               f\"tamamen önlenebilir bir fatura kalemidir: \")\n        r = p.add_run(msg)\n        r.bold = True\n        p.add_run(\", \".join(x[\"name\"] for x in reaktif) + \".\")\n\n    if aud != \"vatandas\":\n        header = [\"Tesis\", \"Skor\", \"Durum\", \"End. oran\", \"PF\", \"Öncelikli bulgu\"]\n        rows = []\n        for r in rs:\n            top = r[\"findings\"][0][\"code\"].replace(\"_\", \" \") if r[\"findings\"] else \"—\"\n            rows.append([r[\"name\"], f\"{r['score']:.0f}\", r[\"risk\"],\n                         nar.pct(r.get(\"ind_ratio_30g\")), nar.num(r.get(\"pf_loaded\")), top])\n        _styled_table(doc, header, rows, [5.2, 1.4, 2.4, 2.0, 1.6, 4.0], risk_col=2)\n    else:\n        onc = krit + risk\n        if onc:\n            p = doc.add_paragraph()\n            p.add_run(\"Bakım planlanan tesisler: \").bold = True\n            p.add_run(\", \".join(r[\"name\"] for r in onc) + \".\")\n\n    onc = krit + risk\n    if onc and aud != \"vatandas\":\n        p = doc.add_paragraph()\n        p.add_run(\"Önerilen öncelik sırası: \").bold = True\n        p.add_run(\"  →  \".join(f\"{i+1}) {r['name']}\" for i, r in enumerate(onc[:5])))\n    doc.add_page_break()\n\n\n# -------------------------------------------------------------- cihaz bölümü\ndef _device_section(doc, r, idx, aud, chart_png=None):\n    h = _heading(doc, f\"{idx}. {r['name']}\", level=1, rule=True)\n    badge = h.add_run(f\"   {r['risk']}\")\n    hx = RISK_HEX[r[\"risk\"]]\n    badge.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n    badge.font.size = Pt(12)\n\n    p = doc.add_paragraph()\n    rr = p.add_run(f\"Dönem: {r['first']} – {r['last']} ({r['days']} gün)   ·   \"\n                   f\"Sağlık skoru: {r['score']:.0f}/100   ·   \"\n                   f\"Tüketim: {nar.num(r['ae_total_kwh'],0,'kWh')}\")\n    rr.font.size = Pt(9); rr.font.color.rgb = GRAY\n\n    if aud == \"vatandas\":\n        durum = {\"İYİ\": \"iyi durumda\", \"İZLENMELİ\": \"izlenmesi gereken\",\n                 \"RİSKLİ\": \"riskli\", \"KRİTİK\": \"kritik\"}[r[\"risk\"]]\n        doc.add_paragraph(f\"Bu tesis şu anda {durum} olarak değerlendiriliyor \"\n                          f\"(100 üzerinden {int(r['score'])} puan).\")\n    elif aud == \"teknik\":\n        header = [\"Metrik\", \"Değer\", \"Eşik\"]\n        rows = [[\"Endüktif oran (30g)\", nar.pct(r.get(\"ind_ratio_30g\")), \"%20 (EPDK)\"],\n                [\"Kapasitif oran (30g)\", nar.pct(r.get(\"cap_ratio_30g\")), \"%15 (EPDK)\"],\n                [\"Güç faktörü (yükte)\", nar.num(r.get(\"pf_loaded\")), \"≥0,95\"],\n                [\"THD-I / THD-V\", f\"{nar.pct(r.get('thd_i'))} / {nar.pct(r.get('thd_v'))}\", \"%15 / %8\"],\n                [\"Akım / gerilim dengesizliği\", f\"{nar.pct(r.get('i_imb'))} / {nar.pct(r.get('v_imb'))}\", \"%10 / %2\"]]\n        if np.isfinite(r.get(\"anomaly_ratio\") or np.nan):\n            rows.append([\"Anomali oranı (IForest)\", nar.pct(r[\"anomaly_ratio\"]), \"%12\"])\n        _styled_table(doc, header, rows, [6.5, 5.0, 4.0])\n        doc.add_paragraph()\n\n    if chart_png and aud != \"vatandas\" and os.path.exists(chart_png):\n        doc.add_picture(chart_png, width=Cm(16.5))\n\n    if r[\"findings\"]:\n        p = doc.add_paragraph()\n        p.add_run(\"Bulgular\" if aud != \"vatandas\" else \"Tespit edilenler\").bold = True\n        for f in sorted(r[\"findings\"], key=lambda x: {\"yüksek\": 0, \"orta\": 1, \"düşük\": 2, \"bilgi\": 3}[x[\"severity\"]]):\n            text = nar._finding_text(f, r, aud)\n            par = doc.add_paragraph(style=\"List Bullet\")\n            m = text.split(\"]\", 1)\n            if text.startswith(\"[\") and len(m) == 2 and aud != \"vatandas\":\n                tag = par.add_run(m[0] + \"]\")\n                hx = SEV_HEX[f[\"severity\"]]\n                tag.font.color.rgb = RGBColor(int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))\n                tag.bold = True\n                par.add_run(m[1])\n            else:\n                par.add_run(text)\n    else:\n        doc.add_paragraph(\"Önemli bir bulgu yok; rutin izleme sürüyor.\")\n\n    recs = nar._recs_for(r[\"findings\"], aud)\n    if recs:\n        p = doc.add_paragraph()\n        p.add_run(\"Önerilen aksiyonlar\" if aud != \"vatandas\" else \"Ne yapılacak?\").bold = True\n        for rec in recs:\n            doc.add_paragraph(rec, style=\"List Number\")\n\n\n# ------------------------------------------------------------------- ana giriş\ndef build_docx(results, daily, aud, out_path, chart_dir):\n    doc = Document()\n    st = doc.styles[\"Normal\"]\n    st.font.name = \"Calibri\"\n    st.font.size = Pt(10.5)\n    for lvl in [\"Heading 1\", \"Heading 2\"]:\n        doc.styles[lvl].font.color.rgb = NAVY\n\n    sec = doc.sections[0]\n    sec.top_margin = sec.bottom_margin = Cm(2.0)\n    sec.left_margin = sec.right_margin = Cm(2.2)\n    _page_number_footer(sec, f\"QAPU Enerji İzleme · {nar.AUDIENCE_META[aud]['baslik']}\")\n\n    _cover(doc, aud, results)\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    _fleet_section(doc, results, aud, fleet_png)\n\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    for i, r in enumerate(rs_sorted, start=2):\n        png = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        _device_section(doc, r, i, aud, chart_png=png if os.path.exists(png) else None)\n\n    doc.save(out_path)\n    return out_path\n\n\ndef generate_docx_reports(results, daily, outdir=\"raporlar\", audiences=None, top_device_charts=8):\n    \"\"\"4 kitle için profesyonel .docx raporları üretir; dosya listesi döner.\"\"\"\n    audiences = audiences or nar.AUDIENCES\n    os.makedirs(outdir, exist_ok=True)\n    chart_dir = os.path.join(outdir, \"grafikler\")\n    os.makedirs(chart_dir, exist_ok=True)\n\n    fleet_png = os.path.join(chart_dir, \"filo_saglik.png\")\n    if not os.path.exists(fleet_png):\n        rep.fleet_chart(results, fleet_png)\n    rs_sorted = sorted(results.values(), key=lambda r: r[\"score\"])\n    for r in rs_sorted[:top_device_charts]:\n        p = os.path.join(chart_dir, f\"cihaz_{r['device_id']}.png\")\n        if not os.path.exists(p):\n            rep.device_chart(daily, r[\"device_id\"], r, p)\n\n    return [build_docx(results, daily, aud, os.path.join(outdir, f\"rapor_{aud}.docx\"), chart_dir)\n            for aud in audiences]\n",
   "id": "Azo7btabJAhk"
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "kE4BHnsnJAhk"
   },
   "execution_count": null,
   "outputs": [],
   "source": "# 6) Veri girişi — qapu.dump dosyasını yükleyin (veya Drive yolu verin)\nimport os\n\nDUMP_PATH = \"\"   # ör. \"/content/drive/MyDrive/qapu.dump\" — boş bırakılırsa yükleme penceresi açılır\nDATA_DIR  = \"data\"\n\nif not DUMP_PATH:\n    try:\n        from google.colab import files\n        up = files.upload()           # qapu.dump seçin\n        DUMP_PATH = list(up)[0]\n    except ImportError:\n        raise SystemExit(\"Colab dışındasınız: DUMP_PATH değişkenine dump yolunu yazın.\")\n\nimport qapu_extract\nqapu_extract.main(DUMP_PATH, DATA_DIR)\nprint(\"Çıkarılan CSV'ler:\", sorted(os.listdir(DATA_DIR)))",
   "id": "kE4BHnsnJAhk"
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "JlxyuNgfJAhl"
   },
   "execution_count": null,
   "outputs": [],
   "source": "# 7) Analiz — günlük özellikler, sağlık skorları, bulgular\nimport pandas as pd, numpy as np\nimport qapu_pm as q\n\ntabs  = q.load_csvs(\"data\")\ndaily = q.build_daily(tabs)\nresults = q.analyze(daily, tabs.get(\"devices\"))\n\nozet = pd.DataFrame([{\n    \"Tesis\": r[\"name\"], \"Gün\": r[\"days\"], \"kWh\": round(r[\"ae_total_kwh\"], 1),\n    \"End.% (30g)\": None if not np.isfinite(r[\"ind_ratio_30g\"] or np.nan) else round(100*r[\"ind_ratio_30g\"], 1),\n    \"Kap.% (30g)\": None if not np.isfinite(r[\"cap_ratio_30g\"] or np.nan) else round(100*r[\"cap_ratio_30g\"], 1),\n    \"PF\": None if not np.isfinite(r[\"pf_loaded\"] or np.nan) else round(r[\"pf_loaded\"], 3),\n    \"THD-I%\": None if not np.isfinite(r[\"thd_i\"] or np.nan) else round(100*r[\"thd_i\"], 1),\n    \"Deng.%\": None if not np.isfinite(r[\"i_imb\"] or np.nan) else round(100*r[\"i_imb\"], 1),\n    \"Skor\": r[\"score\"], \"Durum\": r[\"risk\"], \"Bulgu\": len(r[\"findings\"]),\n} for r in sorted(results.values(), key=lambda x: x[\"score\"])])\nprint(f\"{len(results)} cihaz analiz edildi (yeterli verisi olmayanlar atlandı).\")\nozet",
   "id": "JlxyuNgfJAhl"
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "8XLTj7unJAhl"
   },
   "execution_count": null,
   "outputs": [],
   "source": "# 8) Raporları üret (4 kitle × Word + HTML + MD + grafikler) ve ZIP olarak indir\nimport os, zipfile\nimport qapu_report as rep\nimport qapu_docx as qdocx\n\nzip_path, dosyalar = rep.generate_reports(results, daily, outdir=\"raporlar\")\ndocx_dosyalar = qdocx.generate_docx_reports(results, daily, outdir=\"raporlar\")\n\n# Word dosyalarını ZIP'e ekle\nwith zipfile.ZipFile(zip_path, \"a\", zipfile.ZIP_DEFLATED) as z:\n    for p in docx_dosyalar:\n        z.write(p, os.path.relpath(p, \"raporlar\"))\n\nprint(\"Üretilen dosyalar:\")\nfor f in docx_dosyalar + dosyalar:\n    print(\"  \", f)\nprint(\"ZIP:\", zip_path)\n\ntry:\n    from google.colab import files\n    files.download(zip_path)\nexcept ImportError:\n    pass",
   "id": "8XLTj7unJAhl"
  },
  {
   "cell_type": "code",
   "metadata": {
    "id": "mYmyP-jVJAhl"
   },
   "execution_count": null,
   "outputs": [],
   "source": "# 9) (İsteğe bağlı) Tek bir raporu not defteri içinde görüntüle\nfrom IPython.display import Markdown, display\nimport qapu_narrator as nar\n\nKITLE = \"yonetici_kamu\"   # teknik | yonetici_ozel | yonetici_kamu | vatandas\ndisplay(Markdown(nar.fleet_report(results, KITLE)))\n\n# En riskli cihazın raporu:\nen_riskli = min(results.values(), key=lambda r: r[\"score\"])\ndisplay(Markdown(nar.device_report(en_riskli, KITLE)))",
   "id": "mYmyP-jVJAhl"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "5NO-A-hOJAhl"
   },
   "source": "---\n### Yöntem notları\n* **Reaktif oranlar:** Σ(reaktif Wh) / Σ(aktif Wh), cihaz×gün bazında; \"son 30 gün\" oranı\n  EPDK sınırlarıyla (end. %20 / kap. %15) karşılaştırılır. Negatif enerji örnekleri geçersiz sayılır.\n* **Güç faktörü:** yalnızca yük altındayken (S > cihazın p95 değerinin %5'i) ortalanır —\n  pompa dururken ölçülen anlamsız PF değerleri skoru bozmaz.\n* **THD:** √(h3²+h5²+h7²+h9²)/temel bileşen; faz ortalaması. **Dengesizlik:** max|faz−ort|/ort.\n* **Kalibrasyon farkındalığı:** VRMS medyanı 120–300 V dışındaysa cihaz \"kalibrasyonsuz\" işaretlenir;\n  oransal metrikler (PF, THD, dengesizlik, reaktif oran) kalibrasyondan bağımsız olduğu için yine raporlanır.\n* **Trend/projeksiyon:** günlük serilere doğrusal eğim; sınıra ulaşma süresi ≤90 gün ise bulgu üretilir.\n* **Anomali:** IsolationForest (contamination 0.08), ≥10 günlük cihazlarda, standartlaştırılmış günlük özellik uzayında.\n* Eşikler `qapu_pm.CONFIG` sözlüğünden değiştirilebilir.\n",
   "id": "5NO-A-hOJAhl"
  }
 ]
}