/** * Environment standards from the CP 707 book, for client-side charting only. * * Mirrors temperature / air-quality tables in * `backend/apps/operations/services/cp707_knowledge.py` — that Python module * is authoritative for LLM grading. This TS copy exists so IoT charts can * draw targets without a round trip. Keep both in sync when the book edition * changes. */ /** Target suhu pemeliharaan per umur (buku CP 707). */ const TEMPERATURE_STANDARD = [ { from: 1, to: 2, temp_C: 32 }, { from: 3, to: 4, temp_C: 31 }, { from: 5, to: 7, temp_C: 30 }, { from: 8, to: 14, temp_C: 29 }, { from: 15, to: 21, temp_C: 28 }, { from: 22, to: 28, temp_C: 26 }, { from: 29, to: 35, temp_C: 23 }, { from: 36, to: 99, temp_C: 22 }, ]; /** Kelembapan ideal sama untuk semua umur menurut tabel buku: 50–70 %. */ export const HUMIDITY_RANGE_PCT = { min: 50, max: 70 }; /** Standar kualitas udara (Lampiran 3). */ export const AIR_QUALITY = { /** Amonia: ideal <10 ppm, batas kritis >25 ppm. */ ammoniaSafe_ppm: 10, ammoniaCritical_ppm: 25, /** CO2: ideal <3000 ppm, kritis >3500 ppm. */ co2Ideal_ppm: 3000, }; /** * Standar konsumsi air per 1000 ekor per hari pada suhu 21°C (buku CP 707). * Di atas 21°C kebutuhan naik rata-rata 6.5% per derajat. */ const WATER_CONSUMPTION_STANDARD: { week: number; minLiter: number; maxLiter: number }[] = [ { 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 }, ]; /** Standar konsumsi air (rata-rata min–max) untuk umur hari tertentu, atau null. */ export const getWaterStandardByDay = ( dayAge: number | null | undefined ): { min: number; max: number } | null => { if (typeof dayAge !== 'number' || !Number.isFinite(dayAge) || dayAge <= 0) return null; const week = Math.min(Math.ceil(dayAge / 7), WATER_CONSUMPTION_STANDARD.length); const row = WATER_CONSUMPTION_STANDARD.find((r) => r.week === week); return row ? { min: row.minLiter, max: row.maxLiter } : null; }; /** * Suhu target untuk umur tertentu, atau null bila umurnya tidak masuk akal. * Umur di luar tabel jatuh ke baris terakhir (36+ hari), sesuai isi buku. */ export const getTempStandardByDay = (dayAge: number | null | undefined): number | null => { if (typeof dayAge !== 'number' || !Number.isFinite(dayAge) || dayAge <= 0) return null; const row = TEMPERATURE_STANDARD.find((r) => dayAge >= r.from && dayAge <= r.to); return row ? row.temp_C : null; }; /** * Number out of a display-formatted reading. * * `computeDashboard` hands back strings like "32,1 °C" and "62,8 %" — already * scaled from the raw sensor encoding, which is why they are the safe source — * so the chart has to strip the unit and accept the Indonesian decimal comma. * A plain number passes through unchanged. */ export const parseReading = (value: unknown): number | null => { if (typeof value === 'number') return Number.isFinite(value) ? value : null; if (typeof value !== 'string' || value.trim() === '') return null; const cleaned = value .replace(/[^\d,.-]/g, '') .replace(/\.(?=\d{3}\b)/g, '') .replace(',', '.'); const n = Number(cleaned); return Number.isFinite(n) ? n : null; };