88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
/**
|
||
* 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;
|
||
};
|