""" CP 707 Knowledge Base Sumber: Buku "Manajemen Broiler CP 707" oleh PT Charoen Pokphand Indonesia, Tbk. Edisi Juli 2023 Seluruh standar teknis dari buku panduan CP 707 untuk referensi on-premise AI Insight. TIDAK ada data dari sumber luar — hanya dari buku ini. Port murni Python 3.11+ dari cp707Knowledge.js (tanpa dependensi Django). """ from __future__ import annotations from typing import Any # ─── Standar Performa Mingguan CP 707 ─────────────────────────────────────── PERFORMANCE_STANDARD_WEEKLY: list[dict[str, Any]] = [ {"week": 1, "targetBW_g": 195, "adg_g": 34, "cumFeedConsumption_g": 164.5, "fcr": 0.844}, {"week": 2, "targetBW_g": 499, "adg_g": 50, "cumFeedConsumption_g": 530.5, "fcr": 1.063}, {"week": 3, "targetBW_g": 954, "adg_g": 80, "cumFeedConsumption_g": 1181.5, "fcr": 1.238}, {"week": 4, "targetBW_g": 1543, "adg_g": 88, "cumFeedConsumption_g": 2198.5, "fcr": 1.425}, {"week": 5, "targetBW_g": 2191, "adg_g": 94, "cumFeedConsumption_g": 3461, "fcr": 1.580}, ] # ─── Standar Performa Harian CP 707 (Lampiran 2) ──────────────────────────── PERFORMANCE_STANDARD_DAILY: list[dict[str, Any]] = [ {"day": 1, "bw_g": 57, "adg_g": 15, "mortalityCum_pct": 0.40, "feedDaily_g": 13, "feedCum_g": 13, "fcr": 0.228, "ip": None}, {"day": 2, "bw_g": 73, "adg_g": 16, "mortalityCum_pct": 0.50, "feedDaily_g": 17, "feedCum_g": 30, "fcr": 0.411, "ip": None}, {"day": 3, "bw_g": 90, "adg_g": 17, "mortalityCum_pct": 0.60, "feedDaily_g": 20.5, "feedCum_g": 50.5, "fcr": 0.561, "ip": None}, {"day": 4, "bw_g": 110, "adg_g": 20, "mortalityCum_pct": 0.70, "feedDaily_g": 23, "feedCum_g": 73.5, "fcr": 0.668, "ip": None}, {"day": 5, "bw_g": 134, "adg_g": 24, "mortalityCum_pct": 0.80, "feedDaily_g": 26, "feedCum_g": 99.5, "fcr": 0.743, "ip": None}, {"day": 6, "bw_g": 161, "adg_g": 27, "mortalityCum_pct": 0.90, "feedDaily_g": 31, "feedCum_g": 130.5, "fcr": 0.811, "ip": None}, {"day": 7, "bw_g": 195, "adg_g": 34, "mortalityCum_pct": 1.00, "feedDaily_g": 34, "feedCum_g": 164.5, "fcr": 0.844, "ip": 327}, {"day": 8, "bw_g": 231, "adg_g": 36, "mortalityCum_pct": 1.10, "feedDaily_g": 35, "feedCum_g": 199.5, "fcr": 0.864, "ip": 331}, {"day": 9, "bw_g": 269, "adg_g": 38, "mortalityCum_pct": 1.20, "feedDaily_g": 41, "feedCum_g": 240.5, "fcr": 0.894, "ip": 330}, {"day": 10, "bw_g": 311, "adg_g": 42, "mortalityCum_pct": 1.30, "feedDaily_g": 46, "feedCum_g": 286.5, "fcr": 0.921, "ip": 333}, {"day": 11, "bw_g": 355, "adg_g": 44, "mortalityCum_pct": 1.40, "feedDaily_g": 52, "feedCum_g": 338.5, "fcr": 0.954, "ip": 334}, {"day": 12, "bw_g": 401, "adg_g": 46, "mortalityCum_pct": 1.50, "feedDaily_g": 58, "feedCum_g": 396.5, "fcr": 0.989, "ip": 333}, {"day": 13, "bw_g": 449, "adg_g": 48, "mortalityCum_pct": 1.60, "feedDaily_g": 64, "feedCum_g": 460.5, "fcr": 1.026, "ip": 331}, {"day": 14, "bw_g": 499, "adg_g": 50, "mortalityCum_pct": 1.70, "feedDaily_g": 70, "feedCum_g": 530.5, "fcr": 1.063, "ip": 330}, {"day": 15, "bw_g": 552, "adg_g": 53, "mortalityCum_pct": 1.80, "feedDaily_g": 73, "feedCum_g": 603.5, "fcr": 1.093, "ip": 331}, {"day": 16, "bw_g": 609, "adg_g": 57, "mortalityCum_pct": 1.90, "feedDaily_g": 80, "feedCum_g": 683.5, "fcr": 1.122, "ip": 333}, {"day": 17, "bw_g": 669, "adg_g": 60, "mortalityCum_pct": 2.00, "feedDaily_g": 86, "feedCum_g": 769.5, "fcr": 1.150, "ip": 335}, {"day": 18, "bw_g": 734, "adg_g": 65, "mortalityCum_pct": 2.10, "feedDaily_g": 92, "feedCum_g": 861.5, "fcr": 1.174, "ip": 340}, {"day": 19, "bw_g": 802, "adg_g": 68, "mortalityCum_pct": 2.20, "feedDaily_g": 100, "feedCum_g": 961.5, "fcr": 1.199, "ip": 344}, {"day": 20, "bw_g": 874, "adg_g": 72, "mortalityCum_pct": 2.30, "feedDaily_g": 107, "feedCum_g": 1068.5, "fcr": 1.223, "ip": 349}, {"day": 21, "bw_g": 954, "adg_g": 80, "mortalityCum_pct": 2.40, "feedDaily_g": 113, "feedCum_g": 1181.5, "fcr": 1.238, "ip": 358}, {"day": 22, "bw_g": 1035, "adg_g": 81, "mortalityCum_pct": 2.52, "feedDaily_g": 128, "feedCum_g": 1309.5, "fcr": 1.265, "ip": 362}, {"day": 23, "bw_g": 1117, "adg_g": 82, "mortalityCum_pct": 2.64, "feedDaily_g": 133, "feedCum_g": 1442.5, "fcr": 1.291, "ip": 366}, {"day": 24, "bw_g": 1200, "adg_g": 83, "mortalityCum_pct": 2.76, "feedDaily_g": 139, "feedCum_g": 1581.5, "fcr": 1.318, "ip": 369}, {"day": 25, "bw_g": 1284, "adg_g": 84, "mortalityCum_pct": 2.88, "feedDaily_g": 145, "feedCum_g": 1726.5, "fcr": 1.345, "ip": 371}, {"day": 26, "bw_g": 1369, "adg_g": 85, "mortalityCum_pct": 3.00, "feedDaily_g": 151, "feedCum_g": 1877.5, "fcr": 1.371, "ip": 372}, {"day": 27, "bw_g": 1455, "adg_g": 86, "mortalityCum_pct": 3.12, "feedDaily_g": 157, "feedCum_g": 2034.5, "fcr": 1.398, "ip": 373}, {"day": 28, "bw_g": 1543, "adg_g": 88, "mortalityCum_pct": 3.25, "feedDaily_g": 164, "feedCum_g": 2198.5, "fcr": 1.425, "ip": 374}, {"day": 29, "bw_g": 1633, "adg_g": 90, "mortalityCum_pct": 3.38, "feedDaily_g": 173, "feedCum_g": 2371.5, "fcr": 1.452, "ip": 375}, {"day": 30, "bw_g": 1725, "adg_g": 92, "mortalityCum_pct": 3.52, "feedDaily_g": 176.5, "feedCum_g": 2548, "fcr": 1.477, "ip": 376}, {"day": 31, "bw_g": 1817, "adg_g": 92, "mortalityCum_pct": 3.66, "feedDaily_g": 178, "feedCum_g": 2726, "fcr": 1.500, "ip": 376}, {"day": 32, "bw_g": 1910, "adg_g": 93, "mortalityCum_pct": 3.80, "feedDaily_g": 180, "feedCum_g": 2906, "fcr": 1.521, "ip": 377}, {"day": 33, "bw_g": 2003, "adg_g": 93, "mortalityCum_pct": 3.95, "feedDaily_g": 183, "feedCum_g": 3089, "fcr": 1.542, "ip": 378}, {"day": 34, "bw_g": 2097, "adg_g": 94, "mortalityCum_pct": 4.10, "feedDaily_g": 185, "feedCum_g": 3274, "fcr": 1.561, "ip": 379}, {"day": 35, "bw_g": 2191, "adg_g": 94, "mortalityCum_pct": 4.25, "feedDaily_g": 187, "feedCum_g": 3461, "fcr": 1.580, "ip": 379}, {"day": 36, "bw_g": 2285, "adg_g": 94, "mortalityCum_pct": 4.45, "feedDaily_g": 190, "feedCum_g": 3651, "fcr": 1.598, "ip": 380}, {"day": 37, "bw_g": 2380, "adg_g": 95, "mortalityCum_pct": 4.65, "feedDaily_g": 193, "feedCum_g": 3844, "fcr": 1.615, "ip": 380}, ] # ─── Target Suhu Pemeliharaan per Umur (Buku CP 707) ──────────────────────── TEMPERATURE_STANDARD: list[dict[str, Any]] = [ {"ageDay_from": 1, "ageDay_to": 2, "temp_C": 32, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 3, "ageDay_to": 4, "temp_C": 31, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 5, "ageDay_to": 7, "temp_C": 30, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 8, "ageDay_to": 14, "temp_C": 29, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 15, "ageDay_to": 21, "temp_C": 28, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 22, "ageDay_to": 28, "temp_C": 26, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 29, "ageDay_to": 35, "temp_C": 23, "humidity_pct_min": 50, "humidity_pct_max": 70}, {"ageDay_from": 36, "ageDay_to": 99, "temp_C": 22, "humidity_pct_min": 50, "humidity_pct_max": 70}, ] # ─── Target Efektif Temperatur (TET) per Umur ─────────────────────────────── TARGET_EFFECTIVE_TEMPERATURE: list[dict[str, Any]] = [ {"ageDay_from": 1, "ageDay_to": 2, "tet_C": 32}, {"ageDay_from": 3, "ageDay_to": 4, "tet_C": 31}, {"ageDay_from": 5, "ageDay_to": 7, "tet_C": 30}, {"ageDay_from": 8, "ageDay_to": 14, "tet_C": 29}, {"ageDay_from": 15, "ageDay_to": 21, "tet_C": 27}, {"ageDay_from": 22, "ageDay_to": 28, "tet_C": 25}, {"ageDay_from": 29, "ageDay_to": 35, "tet_C": 22}, {"ageDay_from": 36, "ageDay_to": 99, "tet_C": 21}, ] # ─── Standar Kualitas Udara (Lampiran 3) ──────────────────────────────────── AIR_QUALITY_STANDARD: dict[str, Any] = { "ammonia": { "ideal_pct": "<10 ppm", "warning": 10, # >10 ppm: merusak permukaan paru-paru "critical": 25, # >25 ppm: pertumbuhan menurun "severe": 50, # >50 ppm: pertumbuhan menurun signifikan "note": ">20 ppm lebih rentan terhadap penyakit pernapasan", }, "co2": { "ideal": "<3000 ppm", "critical": 3500, # >3500 ppm: ascites dan kematian tinggi }, "co": { "ideal": "10 ppm", "warning": 50, # >50 ppm mempengaruhi kesehatan "critical": 100, # 100 ppm: meningkatkan angka kematian }, "humidity": { "ideal_after_brooding": "50-60%", "warning_high": 70, # >70% pada suhu >29°C berpengaruh pada pertumbuhan "warning_low": 50, # RH <50% selama brooding berpengaruh pada pertumbuhan }, } # ─── Standar Mortalitas CP 707 ─────────────────────────────────────────────── MORTALITY_THRESHOLDS: dict[str, Any] = { "normal_pct": 5, # <5% mortalitas dianggap normal "warning_pct": 7, # 5-7% perlu perhatian "critical_pct": 7, # >7% kritis # Standar kumulatif per umur (dari tabel harian lampiran 2) "byDayStandard": [ {"day": d["day"], "mortalityCumStd_pct": d["mortalityCum_pct"]} for d in PERFORMANCE_STANDARD_DAILY ], } # ─── Kepadatan Kandang (Buku CP 707) ───────────────────────────────────────── DENSITY_STANDARD: dict[str, Any] = { "openHouse": { "minKgPerM2": 12, "maxKgPerM2": 13, "description": "Kandang terbuka dengan ventilasi alami", }, "closedHouse": { "minKgPerM2": 24, "maxKgPerM2": 30, "description": "Kandang tertutup dapat mencapai 24-30 kg/m2", }, "byHarvestWeight": [ {"minBW_kg": 0.80, "maxBW_kg": 0.99, "density_ekorPerM2_min": 11.0, "density_ekorPerM2_max": 11.1}, {"minBW_kg": 1.00, "maxBW_kg": 1.19, "density_ekorPerM2_min": 10.0, "density_ekorPerM2_max": 10.5}, {"minBW_kg": 1.20, "maxBW_kg": 1.39, "density_ekorPerM2_min": 9.0, "density_ekorPerM2_max": 9.5}, {"minBW_kg": 1.40, "maxBW_kg": 1.59, "density_ekorPerM2_min": 8.0, "density_ekorPerM2_max": 8.5}, {"minBW_kg": 1.60, "maxBW_kg": 1.89, "density_ekorPerM2_min": 7.5, "density_ekorPerM2_max": 8.0}, {"minBW_kg": 1.90, "maxBW_kg": 99, "density_ekorPerM2_min": 7.0, "density_ekorPerM2_max": 7.5}, ], } # ─── Konsumsi Air per 1000 ekor per hari (suhu 21°C) ───────────────────────── WATER_CONSUMPTION_STANDARD: list[dict[str, Any]] = [ {"week": 1, "minLiter": 58, "maxLiter": 65}, {"week": 2, "minLiter": 102, "maxLiter": 115}, {"week": 3, "minLiter": 149, "maxLiter": 167}, {"week": 4, "minLiter": 192, "maxLiter": 216}, {"week": 5, "minLiter": 232, "maxLiter": 261}, {"week": 6, "minLiter": 274, "maxLiter": 308}, {"week": 7, "minLiter": 309, "maxLiter": 347}, {"week": 8, "minLiter": 342, "maxLiter": 385}, ] # Catatan: Di atas 21°C, kebutuhan air meningkat rata-rata 6.5% per kenaikan 1°C WATER_INCREASE_PER_DEGREE_ABOVE_21C_PCT = 6.5 # ─── Program Pencahayaan CP 707 ─────────────────────────────────────────────── LIGHTING_PROGRAM: list[dict[str, Any]] = [ {"ageDay_from": 2, "ageDay_to": 7, "onHours": 23, "darkFrom": "20:00", "darkTo": "21:00"}, {"ageDay_from": 8, "ageDay_to": 14, "onHours": 22, "darkFrom": "20:00", "darkTo": "22:00"}, {"ageDay_from": 15, "ageDay_to": 20, "onHours": 20, "darkFrom": "20:00", "darkTo": "23:00"}, {"ageDay_from": 21, "ageDay_to": 28, "onHours": 20, "darkFrom": "20:00", "darkTo": "24:00"}, {"ageDay_from": 29, "ageDay_to": 99, "onHours": 23, "darkFrom": "20:00", "darkTo": "22:00"}, ] LIGHTING_MIN_INTENSITY_LUX = 25 LIGHTING_BROODING_OPTIMAL_LUX = "40-60" LIGHTING_AFTER_15DAYS_LUX = 5 def _daily_row(day_age: Any) -> dict[str, Any] | None: try: day = int(day_age) except (TypeError, ValueError): return None for row in PERFORMANCE_STANDARD_DAILY: if row["day"] == day: return row return None def _as_float(value: Any) -> float | None: if value is None or isinstance(value, bool): return None try: n = float(value) except (TypeError, ValueError): return None if n != n: # NaN return None return n def _direction_from_delta(delta_pct: float, *, band: float = 5.0) -> str: if delta_pct > band: return "di_atas_standar" if delta_pct < -band: return "di_bawah_standar" return "sesuai_standar" def get_fcr_standard_by_day(day_age: Any) -> float | None: std = _daily_row(day_age) if std: return float(std["fcr"]) try: day = int(day_age) except (TypeError, ValueError): return None if day > 37: return 1.65 return None def get_bw_standard_by_day(day_age: Any) -> float | None: std = _daily_row(day_age) if std: return float(std["bw_g"]) try: day = int(day_age) except (TypeError, ValueError): return None if day > 37: return 2500.0 return None def get_temp_standard_by_day(day_age: Any) -> dict[str, Any]: try: day = int(day_age) except (TypeError, ValueError): return dict(TEMPERATURE_STANDARD[-1]) for row in TEMPERATURE_STANDARD: if row["ageDay_from"] <= day <= row["ageDay_to"]: return dict(row) return dict(TEMPERATURE_STANDARD[-1]) def get_tet_by_day(day_age: Any) -> float: try: day = int(day_age) except (TypeError, ValueError): return 21.0 for row in TARGET_EFFECTIVE_TEMPERATURE: if row["ageDay_from"] <= day <= row["ageDay_to"]: return float(row["tet_C"]) return 21.0 def get_ip_standard_by_day(day_age: Any) -> int | None: """ Standar IP (Indeks Performans / EEF) menurut Lampiran 2 buku CP 707. Kolom IP di tabel buku hanya terisi mulai hari ke-7 dan berhenti di hari ke-37 (nilai 380). Di luar rentang itu buku TIDAK menyatakan standar, jadi fungsi ini mengembalikan None — jangan menggantinya dengan tebakan. """ std = _daily_row(day_age) if std is None: return None ip = std.get("ip") return int(ip) if isinstance(ip, (int, float)) else None def get_ip_standard_range() -> dict[str, int] | None: """Rentang IP yang tercantum di buku, untuk konteks saat hari di luar tabel.""" values = [int(d["ip"]) for d in PERFORMANCE_STANDARD_DAILY if isinstance(d.get("ip"), (int, float))] if not values: return None return {"min": min(values), "max": max(values), "lastDay": 37} def get_mortality_cum_std_by_day(day_age: Any) -> float | None: std = _daily_row(day_age) if std: return float(std["mortalityCum_pct"]) try: day = int(day_age) except (TypeError, ValueError): return None if day > 37: return 4.65 + ((day - 37) * 0.1) return None def analyze_fcr(actual_fcr: Any, day_age: Any) -> dict[str, Any]: actual = _as_float(actual_fcr) std_fcr = get_fcr_standard_by_day(day_age) if actual is None or std_fcr is None: return { "actual": actual, "standard": std_fcr, "delta_pct": None, "direction": "unknown", "status": "unknown", "message": "Umur di luar standar tabel CP 707." if std_fcr is None else "FCR aktual tidak tersedia.", } delta_pct = round(((actual - std_fcr) / std_fcr) * 100, 1) direction = _direction_from_delta(delta_pct) if delta_pct > 10: status = "critical" message = ( f"FCR {actual:.2f} melebihi standar CP 707 ({std_fcr:.3f}) sebesar {delta_pct:.1f}% " f"pada umur hari ke-{day_age}. Periksa potensi pakan tercecer dan kualitas pakan." ) elif delta_pct > 5: status = "warning" message = ( f"FCR {actual:.2f} sedikit di atas standar CP 707 ({std_fcr:.3f}) " f"pada umur hari ke-{day_age}. Atur ketinggian piringan pakan dan evaluasi fines dalam pakan." ) elif delta_pct < -5: status = "ok" message = ( f"FCR {actual:.2f} lebih baik dari standar CP 707 ({std_fcr:.3f}) " f"pada umur hari ke-{day_age}. Pertahankan manajemen pakan." ) else: status = "ok" message = f"FCR {actual:.2f} sesuai standar CP 707 ({std_fcr:.3f}) pada umur hari ke-{day_age}." return { "actual": actual, "standard": std_fcr, "delta_pct": float(delta_pct), "direction": direction, "status": status, "message": message, } def analyze_bw(actual_bw_g: Any, day_age: Any) -> dict[str, Any]: actual = _as_float(actual_bw_g) std_bw = get_bw_standard_by_day(day_age) if actual is None or std_bw is None: return { "actual": actual, "standard": std_bw, "delta_pct": None, "direction": "unknown", "status": "unknown", "message": "Umur di luar standar tabel CP 707." if std_bw is None else "Bobot aktual tidak tersedia.", } delta_pct = round(((actual - std_bw) / std_bw) * 100, 1) direction = _direction_from_delta(delta_pct) if delta_pct < -15: status = "critical" message = ( f"Bobot badan aktual {actual:g}g jauh di bawah standar CP 707 ({std_bw:g}g) " f"pada hari ke-{day_age}. Deviasi {abs(delta_pct):.1f}%. Lakukan grading segera." ) elif delta_pct < -5: status = "warning" message = ( f"Bobot badan aktual {actual:g}g sedikit di bawah standar CP 707 ({std_bw:g}g) " f"pada hari ke-{day_age}. Deviasi {abs(delta_pct):.1f}%. Tingkatkan stimulasi pakan." ) elif delta_pct > 10: status = "ok" message = ( f"Bobot badan aktual {actual:g}g melampaui standar CP 707 ({std_bw:g}g) " f"pada hari ke-{day_age}. Pertumbuhan sangat baik." ) else: status = "ok" message = f"Bobot badan aktual {actual:g}g sesuai standar CP 707 ({std_bw:g}g) pada hari ke-{day_age}." return { "actual": actual, "standard": std_bw, "delta_pct": float(delta_pct), "direction": direction, "status": status, "message": message, } def analyze_mortality(mortality_rate_pct: Any, day_age: Any) -> dict[str, Any]: rate = _as_float(mortality_rate_pct) std_mortality = get_mortality_cum_std_by_day(day_age) if rate is None: return { "status": "unknown", "direction": "unknown", "stdMortality": std_mortality, "message": "Data mortalitas tidak tersedia.", } if rate > MORTALITY_THRESHOLDS["critical_pct"]: status = "critical" direction = "di_atas_standar" message = ( f"Mortalitas kumulatif {rate:.2f}% melebihi batas kritis CP 707 (>7%). " "Lakukan nekropsi darurat dan perketat biosekuriti." ) elif rate >= MORTALITY_THRESHOLDS["warning_pct"]: status = "warning" direction = "di_atas_standar" message = f"Mortalitas kumulatif {rate:.2f}% perlu diwaspadai. Standar CP 707 <5%." elif std_mortality is not None and rate > std_mortality * 1.5: status = "warning" direction = "di_atas_standar" message = ( f"Mortalitas {rate:.2f}% melebihi standar kumulatif harian CP 707 " f"({std_mortality:.2f}%) pada hari ke-{day_age}." ) else: status = "ok" direction = "sesuai_standar" if (std_mortality is None or rate <= std_mortality) else "di_atas_standar" message = f"Mortalitas {rate:.2f}% dalam batas normal CP 707 (<5%)." return { "status": status, "direction": direction, "stdMortality": std_mortality, "message": message, } def analyze_environment( temp_c: Any, humidity_pct: Any, ammonia_ppm: Any, day_age: Any, ) -> dict[str, Any]: temp = _as_float(temp_c) humidity = _as_float(humidity_pct) ammonia = _as_float(ammonia_ppm) if temp is None: return { "status": "unknown", "direction": "unknown", "issues": [], "tempStd": get_temp_standard_by_day(day_age), "tet": get_tet_by_day(day_age), "message": "Data suhu tidak tersedia.", } if humidity is None: humidity = 65.0 if ammonia is None: ammonia = 0.0 temp_std = get_temp_standard_by_day(day_age) tet = get_tet_by_day(day_age) issues: list[str] = [] overall_status = "ok" if temp > temp_std["temp_C"] + 4: issues.append( f"Suhu {temp:.1f}°C jauh di atas target CP 707 ({temp_std['temp_C']}°C) " f"untuk umur hari ke-{day_age}. Nyalakan cooling pad/exhaust fan segera." ) overall_status = "critical" elif temp > temp_std["temp_C"] + 2: issues.append( f"Suhu {temp:.1f}°C di atas target CP 707 ({temp_std['temp_C']}°C) " f"untuk umur hari ke-{day_age}. Tingkatkan ventilasi." ) if overall_status != "critical": overall_status = "warning" elif temp < temp_std["temp_C"] - 3: issues.append( f"Suhu {temp:.1f}°C di bawah target CP 707 ({temp_std['temp_C']}°C). Nyalakan heater." ) if overall_status != "critical": overall_status = "warning" if humidity < temp_std["humidity_pct_min"]: issues.append( f"Kelembapan {humidity:.1f}% di bawah standar CP 707 " f"({temp_std['humidity_pct_min']}-{temp_std['humidity_pct_max']}%)." ) if overall_status != "critical": overall_status = "warning" elif humidity > temp_std["humidity_pct_max"]: issues.append( f"Kelembapan {humidity:.1f}% di atas standar CP 707 " f"({temp_std['humidity_pct_min']}-{temp_std['humidity_pct_max']}%). " "Periksa kebocoran nipple dan sekam basah." ) if overall_status != "critical": overall_status = "warning" if ammonia > AIR_QUALITY_STANDARD["ammonia"]["critical"]: issues.append( f"Amonia {ammonia:.1f} ppm di atas batas kritis CP 707 (>25 ppm). " "Pertumbuhan ayam menurun. Tingkatkan ventilasi segera dan gemburkan sekam basah." ) overall_status = "critical" elif ammonia > AIR_QUALITY_STANDARD["ammonia"]["warning"]: issues.append( f"Amonia {ammonia:.1f} ppm melebihi batas aman CP 707 (<10 ppm). Tingkatkan sirkulasi udara." ) if overall_status != "critical": overall_status = "warning" return { "status": overall_status, "direction": "sesuai_standar" if overall_status == "ok" else "di_atas_standar", "issues": issues, "tempStd": temp_std, "tet": tet, "message": ( " ".join(issues) if issues else f"Lingkungan kandang sesuai standar CP 707 untuk umur hari ke-{day_age}." ), } def build_book_reference_context(day_age: Any) -> str: std_bw = get_bw_standard_by_day(day_age) std_fcr = get_fcr_standard_by_day(day_age) std_mortality = get_mortality_cum_std_by_day(day_age) temp_std = get_temp_standard_by_day(day_age) tet = get_tet_by_day(day_age) return f""" === REFERENSI STANDAR BUKU CP 707 (SUMBER TUNGGAL - PT CHAROEN POKPHAND INDONESIA) === Umur ayam hari ke-{day_age}: - Target bobot badan: {std_bw if std_bw is not None else 'N/A'} gram - Target FCR: {std_fcr if std_fcr is not None else 'N/A'} - Target mortalitas kumulatif normal: <5% (standar CP 707) - Target mortalitas kumulatif standar hari ke-{day_age}: {str(std_mortality) + '%' if std_mortality is not None else 'N/A'} - Target suhu kandang: {temp_std['temp_C']}°C - Target kelembapan: {temp_std['humidity_pct_min']}%-{temp_std['humidity_pct_max']}% - Target Efektif Temperatur (TET): {tet}°C - Batas amonia aman: <10 ppm (ideal), >25 ppm = pertumbuhan menurun (dari Lampiran 3 CP 707) - Batas CO2 aman: <3000 ppm (>3500 ppm = ascites & kematian tinggi) - Konsumsi air normal: 2-2.5x konsumsi pakan CATATAN PENTING: - Analisis HANYA berdasarkan standar buku CP 707 - Jika ada data yang tidak ada di buku CP 707, nyatakan "data tidak tersedia di buku CP 707" - Risiko yang disebutkan HARUS berdasarkan data aktual vs standar CP 707 === END REFERENSI === """ def build_cp707_standard_block(context_data: dict[str, Any] | None) -> str: """ Blok standar CP 707 berlabel satuan untuk hari yang sedang dilihat (setara buildCp707StandardBlock di aiInsights.js lama). """ if not isinstance(context_data, dict): return "" raw = None for key in ("hari_ke", "hari_terakhir", "currentDay", "dayAge"): if context_data.get(key) is not None: raw = context_data.get(key) break day_f = _as_float(raw) if day_f is None or day_f <= 0: return "" day = int(day_f) ip = get_ip_standard_by_day(day) ip_range = get_ip_standard_range() bw = get_bw_standard_by_day(day) fcr = get_fcr_standard_by_day(day) mort = get_mortality_cum_std_by_day(day) lines = [ f"- Bobot badan standar hari ke-{day}: {str(bw) + ' gram' if bw is not None else 'tidak tercantum di buku'}", f"- FCR standar hari ke-{day}: {str(fcr) + ' (rasio, tanpa satuan)' if fcr is not None else 'tidak tercantum di buku'}", f"- Mortalitas kumulatif standar hari ke-{day}: {str(mort) + ' %' if mort is not None else 'tidak tercantum di buku'}", ] if mort is not None: hidup = round((100 - mort) * 100) / 100 lines.append( f"- Persen hidup standar hari ke-{day}: {hidup} % " "(turunan langsung dari mortalitas standar di atas — pakai angka ini, jangan menghitung sendiri)" ) else: lines.append(f"- Persen hidup standar hari ke-{day}: tidak tersedia") if ip is not None: lines.append(f"- IP/EEF standar hari ke-{day}: {ip} (indeks, TANPA satuan)") else: last_day = ip_range["lastDay"] if ip_range else 37 rng = f"{ip_range['min']}-{ip_range['max']}" if ip_range else "327-380" lines.append( f"- IP/EEF standar hari ke-{day}: TIDAK tercantum di buku. " f"Kolom IP pada Lampiran 2 hanya terisi hari ke-7 s/d ke-{last_day} dengan rentang {rng}. " "Jangan mengarang standar untuk hari ini." ) joined = "\n".join(lines) return f""" ═══════════════════════════════════════════════ STANDAR CP 707 UNTUK HARI INI (angka resmi dari Lampiran 2 buku) {joined} ATURAN WAJIB saat membandingkan dengan standar: - Pakai HANYA angka di blok ini sebagai standar. DILARANG mengambil angka standar dari tabel mentah di kutipan buku di bawah — kolomnya tidak berjudul, dan angka pakan (gram) sering tertukar menjadi standar IP/EEF. - Sebutkan satuan dengan benar: gram untuk bobot dan pakan, persen untuk mortalitas, dan IP/EEF adalah indeks TANPA satuan. - Jika standar untuk suatu metrik tidak tercantum, tulis "standar tidak tersedia di buku untuk hari ini" — jangan mengganti dengan angka lain. - Semua angka aktual harus berasal dari [Data Halaman (JSON)]. Dilarang menghitung sendiri atau mengarang angka yang tidak ada di sana. ═══════════════════════════════════════════════""" def _nested_get(obj: Any, *path: str) -> Any: cur = obj for key in path: if not isinstance(cur, dict): return None cur = cur.get(key) return cur def _first_number(*candidates: Any) -> float | None: for value in candidates: n = _as_float(value) if n is not None: return n return None def _resolve_day_age(context_pack: dict[str, Any]) -> int | None: day = _first_number( context_pack.get("hari_ke"), _nested_get(context_pack, "metadata", "currentDay"), _nested_get(context_pack, "cycle", "currentAgeDay"), context_pack.get("ageDay"), context_pack.get("umurHari"), context_pack.get("currentDay"), context_pack.get("dayAge"), ) if day is None or day <= 0: return None return int(day) def _resolve_bw_grams(context_pack: dict[str, Any]) -> float | None: """Bobot dalam gram. Field `_kg` hanya untuk legacy payload (dikonversi ×1000).""" grams = _first_number( context_pack.get("bobot_rata_rata_gram"), context_pack.get("bobot_iot_gram_terakhir"), context_pack.get("bobot_iot_gram"), _nested_get(context_pack, "weightStats", "averageWeight", "value"), _nested_get(context_pack, "weight", "current", "averageWeight"), context_pack.get("berat_rata_rata"), ) if grams is not None: return grams kg = _first_number( context_pack.get("bobot_iot_kg_terakhir"), context_pack.get("bobot_iot_kg"), ) if kg is not None: return kg * 1000.0 return None def _resolve_mortality(context_pack: dict[str, Any]) -> float | None: return _first_number( context_pack.get("mortalitas_kumulatif_pct"), context_pack.get("mortalitas_persen"), context_pack.get("mortalityRate"), context_pack.get("mortality_rate_pct"), _nested_get(context_pack, "chickenCounting", "dashboardSummary", "mortalityRate"), _nested_get(context_pack, "chickenCounting", "mortality", "mortalityCount"), ) def _resolve_iot(context_pack: dict[str, Any]) -> dict[str, Any]: for key in ("iotPanel", "panel_iot", "telemetry", "iotData", "iot"): raw = context_pack.get(key) if isinstance(raw, dict): display = raw.get("display") if isinstance(display, dict): return display return raw # Flat InsightContext fields (rebuild) return context_pack def analyze_with_cp707_standards(context_pack: dict[str, Any] | None) -> dict[str, Any]: """Analisis komprehensif; toleran terhadap field InsightContext rebuild dan pack lama.""" pack = context_pack if isinstance(context_pack, dict) else {} day_age = _resolve_day_age(pack) result: dict[str, Any] = {"dayAge": day_age, "analyses": {}} actual_bw = _resolve_bw_grams(pack) if actual_bw is not None and day_age is not None: result["analyses"]["bw"] = analyze_bw(actual_bw, day_age) actual_fcr = _first_number( pack.get("fcr_terakhir"), pack.get("fcr"), _nested_get(pack, "fcr_eef", "fcr", "actual"), _nested_get(pack, "weight", "current", "fcr"), _nested_get(pack, "fcrSummary", "current"), ) if actual_fcr is not None and day_age is not None: result["analyses"]["fcr"] = analyze_fcr(actual_fcr, day_age) mortality_rate = _resolve_mortality(pack) if mortality_rate is not None: result["analyses"]["mortality"] = analyze_mortality(mortality_rate, day_age) iot = _resolve_iot(pack) temp = _first_number( iot.get("suhu_rata_rata_C"), iot.get("avgTemp"), pack.get("suhu_rata_rata_C"), ) hum = _first_number( iot.get("kelembapan_persen"), iot.get("humidity"), pack.get("kelembapan_persen"), ) ammonia = _first_number( iot.get("amonia_ppm"), iot.get("ammonia"), pack.get("amonia_ppm"), ) if temp is not None and day_age is not None: result["analyses"]["environment"] = analyze_environment( temp, hum if hum is not None else 65.0, ammonia if ammonia is not None else 0.0, day_age, ) return result