import { ScalePredictionRangeResponse, SensorWeightSummaryResponse, DailyWeightPoint, DailyWeightGainPoint, DeviceWeightData, WeightStatistics, } from '../types.ts'; import { getTargetWeightForDay } from './weightUtils.ts'; const MS_PER_DAY = 24 * 60 * 60 * 1000; interface DeviceInfo { id: string; name: string; status: boolean; last_online: string; } function parseDateOnly(dateString: string): Date | null { const [year, month, day] = dateString.split('-').map(Number); if (!year || !month || !day) { return null; } return new Date(year, month - 1, day); } function getDayOfCycle(dateString: string, cycleStartDate: Date): number | null { const pointDate = parseDateOnly(dateString); if (!pointDate) { return null; } const startDate = new Date(cycleStartDate); startDate.setHours(0, 0, 0, 0); pointDate.setHours(0, 0, 0, 0); const diffMs = pointDate.getTime() - startDate.getTime(); if (diffMs < 0) { return null; } return Math.floor(diffMs / MS_PER_DAY) + 1; } /** * Process scale prediction range data into daily weight points */ export function processDailyWeightData( scaleData: ScalePredictionRangeResponse, cycleStartDate: Date, maxDay?: number ): DailyWeightPoint[] { const dailyMap = new Map< string, { weights: number[]; chickenCounts: number[]; date: string; day: number; } >(); // Group data by date scaleData.data.forEach((dayData) => { const fallbackAge = dayData.items[0]?.age; const dayOfCycle = getDayOfCycle(dayData.date, cycleStartDate) ?? fallbackAge ?? 1; if (dayOfCycle < 1) { return; } if (maxDay !== undefined && dayOfCycle > maxDay) { return; } const weights: number[] = []; const chickenCounts: number[] = []; dayData.items.forEach((item) => { if (item.avg_weight_actual > 0 && item.chicken_count > 0) { weights.push(item.avg_weight_actual); chickenCounts.push(item.chicken_count); } }); if (weights.length > 0) { dailyMap.set(dayData.date, { weights, chickenCounts, date: dayData.date, day: dayOfCycle, }); } }); // Convert to DailyWeightPoint array const result: DailyWeightPoint[] = []; dailyMap.forEach((dayInfo) => { // Calculate weighted average let totalWeight = 0; let totalChickens = 0; dayInfo.weights.forEach((weight, idx) => { const count = dayInfo.chickenCounts[idx] ?? 0; if (count <= 0) { return; } totalWeight += weight * count; totalChickens += count; }); const avgWeight = totalChickens > 0 ? totalWeight / totalChickens : 0; // Skip if no actual weight data if (avgWeight === 0 || totalChickens === 0) { return; } const minWeight = Math.min(...dayInfo.weights); const maxWeight = Math.max(...dayInfo.weights); const targetWeight = getTargetWeightForDay(dayInfo.day) || 0; result.push({ day: dayInfo.day, date: dayInfo.date, actual: Math.round(avgWeight), target: targetWeight, min: Math.round(minWeight), max: Math.round(maxWeight), chickenCount: totalChickens, }); }); // Sort by day return result.sort((a, b) => a.day - b.day); } /** * Calculate daily weight gain from daily weight data */ export function calculateDailyWeightGain( dailyWeightData: DailyWeightPoint[] ): DailyWeightGainPoint[] { const result: DailyWeightGainPoint[] = []; for (let i = 1; i < dailyWeightData.length; i++) { const today = dailyWeightData[i]; const yesterday = dailyWeightData[i - 1]; if (!today || !yesterday) { continue; } // Skip if either day has no actual weight if (!today.actual || !yesterday.actual) { continue; } const gain = today.actual - yesterday.actual; const targetGain = (today.target || 0) - (yesterday.target || 0); // Determine status let status: 'above' | 'near' | 'below'; const threshold = Math.abs(targetGain) * 0.1; // 10% threshold if (gain >= targetGain + threshold) { status = 'above'; } else if (gain >= targetGain - threshold) { status = 'near'; } else { status = 'below'; } result.push({ day: today.day, date: today.date, gain, targetGain, status, }); } return result; } /** * Process sensor summary data into device weight data */ export function processDeviceWeightData( summaryData: SensorWeightSummaryResponse, deviceInfoList: DeviceInfo[] ): DeviceWeightData[] { return summaryData.data .filter((sensor) => { // Skip devices with no weight data or no chickens if ( !sensor.avg_weight_actual || sensor.avg_weight_actual <= 0 || !sensor.chicken_count || sensor.chicken_count <= 0 ) { return false; } return true; }) .map((sensor) => { const deviceInfo = deviceInfoList.find((d) => d.id === sensor.device_id); return { deviceId: sensor.device_id, deviceName: sensor.device_name, avgWeight: Math.round(sensor.avg_weight_actual), chickenCount: sensor.chicken_count, isOnline: deviceInfo?.status ?? true, lastOnline: deviceInfo?.last_online ?? new Date().toISOString(), }; }); } /** * Calculate weight statistics from sensor summary data */ export function calculateWeightStatistics( summaryData: SensorWeightSummaryResponse, yesterdaySummary?: SensorWeightSummaryResponse ): WeightStatistics { // Calculate weighted average weight - only include sensors with data let totalWeight = 0; let totalChickens = 0; summaryData.data.forEach((sensor) => { // Skip sensors with no data if (sensor.avg_weight_actual > 0 && sensor.chicken_count > 0) { totalWeight += sensor.avg_weight_actual * sensor.chicken_count; totalChickens += sensor.chicken_count; } }); const avgWeight = totalChickens > 0 ? totalWeight / totalChickens : 0; // Calculate daily gain let yesterdayAvgWeight = 0; if (yesterdaySummary) { let yTotalWeight = 0; let yTotalChickens = 0; yesterdaySummary.data.forEach((sensor) => { if (sensor.avg_weight_actual > 0 && sensor.chicken_count > 0) { yTotalWeight += sensor.avg_weight_actual * sensor.chicken_count; yTotalChickens += sensor.chicken_count; } }); yesterdayAvgWeight = yTotalChickens > 0 ? yTotalWeight / yTotalChickens : 0; } const dailyGain = yesterdayAvgWeight > 0 ? avgWeight - yesterdayAvgWeight : 0; // Calculate uniformity (CV - Coefficient of Variation) // Lower CV means better uniformity const deviceWeights = summaryData.data .filter((s) => s.avg_weight_actual > 0 && s.chicken_count > 0) .map((s) => s.avg_weight_actual); const mean = avgWeight; const variance = deviceWeights.length > 0 ? deviceWeights.reduce((sum, w) => sum + Math.pow(w - mean, 2), 0) / deviceWeights.length : 0; const stdDev = Math.sqrt(variance); const cv = mean > 0 ? (stdDev / mean) * 100 : 0; const uniformity = Math.max(0, Math.min(100, 100 - cv)); // Convert CV to uniformity score // Determine status const getAdgStatus = (adg: number): 'ok' | 'warning' | 'critical' => { if (adg >= 60) return 'ok'; if (adg >= 40) return 'warning'; return 'critical'; }; const getUniformityStatus = (uniformity: number): 'ok' | 'warning' | 'critical' => { if (uniformity >= 85) return 'ok'; if (uniformity >= 75) return 'warning'; return 'critical'; }; return { averageWeight: { value: Math.round(avgWeight), unit: 'g', status: 'ok', // Will be updated with actual target comparison }, avgDailyGain: { value: Math.round(dailyGain), unit: 'g/hari', status: getAdgStatus(dailyGain), }, uniformity: { value: Math.round(uniformity), unit: '%', status: getUniformityStatus(uniformity), }, }; }