You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1094 lines
45 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Haoliang.Core.Services;
using Haoliang.Data.Repositories;
using Haoliang.Models.Models.Device;
using Haoliang.Models.Models.Production;
using Haoliang.Models.Models.System;
using Haoliang.Models.Models.DataCollection;
namespace Haoliang.Core.Services
{
public class ProductionStatisticsService : IProductionStatisticsService
{
private readonly IProductionRepository _productionRepository;
private readonly IDeviceRepository _deviceRepository;
private readonly ISystemRepository _systemRepository;
private readonly IAlarmRepository _alarmRepository;
private readonly ICollectionRepository _collectionRepository;
public ProductionStatisticsService(
IProductionRepository productionRepository,
IDeviceRepository deviceRepository,
ISystemRepository systemRepository,
IAlarmRepository alarmRepository,
ICollectionRepository collectionRepository)
{
_productionRepository = productionRepository;
_deviceRepository = deviceRepository;
_systemRepository = systemRepository;
_alarmRepository = alarmRepository;
_collectionRepository = collectionRepository;
}
public async Task<ProductionTrendAnalysis> CalculateProductionTrendsAsync(int deviceId, DateTime startDate, DateTime endDate)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null)
throw new KeyNotFoundException($"Device {deviceId} not found");
var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(
deviceId, startDate, endDate);
var dailyData = new List<DailyProduction>();
decimal totalProduction = 0;
decimal sumSquaredDifferences = 0;
decimal mean = 0;
int dayCount = 0;
// Group records by date
var groupedByDate = productionRecords.GroupBy(r => r.Created.Date);
foreach (var dateGroup in groupedByDate)
{
var date = dateGroup.Key;
var dayRecords = dateGroup.ToList();
var dayProduction = dayRecords.Sum(r => r.Quantity);
// Get target for this date (if available)
var target = await GetProductionTargetAsync(deviceId, date);
dailyData.Add(new DailyProduction
{
Date = date,
Quantity = dayProduction,
Target = target,
Efficiency = target > 0 ? (dayProduction / target) * 100 : 0,
Records = dayRecords
});
totalProduction += dayProduction;
dayCount++;
}
if (dayCount > 0)
{
mean = totalProduction / dayCount;
// Calculate variance
foreach (var day in dailyData)
{
sumSquaredDifferences += (decimal)Math.Pow((double)(day.Quantity - mean), 2);
}
}
decimal productionVariance = dayCount > 0 ? (decimal)Math.Sqrt((double)(sumSquaredDifferences / dayCount)) : 0;
// Calculate trend using linear regression
var trendCoefficient = CalculateLinearRegressionTrend(dailyData);
var trendDirection = DetermineTrendDirection(trendCoefficient);
return new ProductionTrendAnalysis
{
DeviceId = deviceId,
DeviceName = device.Name,
PeriodStart = startDate,
PeriodEnd = endDate,
TotalProduction = totalProduction,
AverageDailyProduction = mean,
ProductionVariance = productionVariance,
TrendCoefficient = trendCoefficient,
TrendDirection = trendDirection,
DailyData = dailyData
};
}
public async Task<ProductionReport> GenerateProductionReportAsync(ReportFilter filter)
{
var summaryItems = new List<ProductionSummaryItem>();
var metadata = new ReportMetadata();
// Get production records based on filter
var records = await _productionRepository.GetProductionRecordsByFilterAsync(filter);
// Group by device and program
var groupedData = records.GroupBy(r => new { r.DeviceId, r.ProgramName });
foreach (var group in groupedData)
{
var deviceId = group.Key.DeviceId;
var programName = group.Key.ProgramName;
var device = await _deviceRepository.GetByIdAsync(deviceId);
var totalQuantity = group.Sum(r => r.Quantity);
var totalTarget = group.Sum(r => r.TargetQuantity);
// Calculate quality rate
var qualityRate = await CalculateQualityRateAsync(deviceId, programName, filter.StartDate, filter.EndDate);
// Calculate runtime and downtime
var runtime = CalculateRuntime(group.ToList());
var downtime = await CalculateDowntimeAsync(deviceId, filter.StartDate, filter.EndDate);
summaryItems.Add(new ProductionSummaryItem
{
DeviceId = deviceId,
DeviceName = device?.Name ?? "Unknown",
ProgramName = programName,
Quantity = totalQuantity,
TargetQuantity = totalTarget,
Efficiency = totalTarget > 0 ? (totalQuantity / totalTarget) * 100 : 0,
QualityRate = qualityRate,
Runtime = runtime,
Downtime = downtime
});
}
metadata = CalculateReportMetadata(summaryItems, filter);
return new ProductionReport
{
ReportDate = DateTime.Now,
ReportType = filter.ReportType,
SummaryItems = summaryItems,
Metadata = metadata
};
}
public async Task<EfficiencyMetrics> CalculateEfficiencyMetricsAsync(EfficiencyFilter filter)
{
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
(await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList();
var metrics = new List<EfficiencyMetrics>();
var hourlyData = new List<HourlyEfficiency>();
foreach (var deviceId in deviceIds)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null) continue;
var efficiencyMetrics = await CalculateDeviceEfficiencyAsync(deviceId, filter);
metrics.Add(efficiencyMetrics);
// Add hourly data if requested
if (filter.GroupBy == GroupBy.Hour)
{
hourlyData.AddRange(efficiencyMetrics.HourlyData);
}
}
// Calculate aggregate metrics if multiple devices
var aggregatedMetrics = AggregateEfficiencyMetrics(metrics);
return aggregatedMetrics;
}
public async Task<QualityAnalysis> PerformQualityAnalysisAsync(QualityFilter filter)
{
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
(await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList();
var totalProduced = 0m;
var totalGood = 0m;
var qualityMetrics = new List<QualityMetric>();
var defectAnalysis = new List<DefectAnalysis>();
foreach (var deviceId in deviceIds)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null) continue;
// Get production records
var records = await _productionRepository.GetProductionRecordsByFilterAsync(new ReportFilter
{
DeviceIds = new List<int> { deviceId },
StartDate = filter.StartDate,
EndDate = filter.EndDate,
ProgramNames = filter.ProgramNames
});
var deviceTotalProduced = records.Sum(r => r.Quantity);
var deviceTotalGood = records.Where(r => r.IsGood).Sum(r => r.Quantity);
totalProduced += deviceTotalProduced;
totalGood += deviceTotalGood;
// Calculate quality metrics for this device
var deviceMetrics = await CalculateDeviceQualityMetricsAsync(deviceId, filter);
qualityMetrics.AddRange(deviceMetrics);
// Perform defect analysis
var deviceDefects = await AnalyzeDefectsAsync(deviceId, filter);
defectAnalysis.AddRange(deviceDefects);
}
var qualityRate = totalProduced > 0 ? (totalGood / totalProduced) * 100 : 0;
var defectRate = totalProduced > 0 ? 100 - qualityRate : 0;
return new QualityAnalysis
{
DeviceIds = deviceIds,
DeviceNames = deviceIds.Select(id => _deviceRepository.GetByIdAsync(id).Result?.Name ?? "Unknown").ToList(),
PeriodStart = filter.StartDate,
PeriodEnd = filter.EndDate,
TotalProduced = totalProduced,
TotalGood = totalGood,
QualityRate = qualityRate,
DefectRate = defectRate,
QualityMetrics = qualityMetrics,
DefectAnalysis = defectAnalysis
};
}
public async Task<DashboardSummary> GetDashboardSummaryAsync(DashboardFilter filter)
{
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
(await _deviceRepository.GetAllDevicesAsync()).Select(d => d.Id).ToList();
var deviceSummaries = new List<DeviceSummary>();
var activeAlerts = new List<AlertSummary>();
int totalDevices = deviceIds.Count;
int activeDevices = 0;
int offlineDevices = 0;
decimal totalProductionToday = 0;
decimal totalProductionThisWeek = 0;
decimal totalProductionThisMonth = 0;
decimal overallEfficiency = 0;
decimal qualityRate = 0;
int efficiencyCount = 0;
foreach (var deviceId in deviceIds)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null) continue;
// Get device status
var deviceStatus = await GetDeviceCurrentStatusAsync(deviceId);
// Get today's production
var todayProduction = await GetDeviceProductionForDateAsync(deviceId, DateTime.Today);
// Get weekly and monthly production
var weekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek);
var weekProduction = await GetDeviceProductionForDateRangeAsync(deviceId, weekStart, DateTime.Today);
var monthStart = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
var monthProduction = await GetDeviceProductionForDateRangeAsync(deviceId, monthStart, DateTime.Today);
// Calculate efficiency and quality
var efficiency = await CalculateDeviceEfficiencyAsync(deviceId, new EfficiencyFilter
{
DeviceIds = new List<int> { deviceId },
StartDate = DateTime.Today,
EndDate = DateTime.Today
});
var quality = await CalculateQualityRateAsync(deviceId, null, DateTime.Today, DateTime.Today);
deviceSummaries.Add(new DeviceSummary
{
DeviceId = deviceId,
DeviceName = device.Name,
Status = deviceStatus.Status,
TodayProduction = todayProduction,
Efficiency = efficiency.Oee,
QualityRate = quality,
Runtime = deviceStatus.Runtime,
CurrentProgram = deviceStatus.CurrentProgram
});
totalProductionToday += todayProduction;
totalProductionThisWeek += weekProduction;
totalProductionThisMonth += monthProduction;
overallEfficiency += efficiency.Oee;
qualityRate += quality;
efficiencyCount++;
// Count devices by status
if (deviceStatus.Status == DeviceStatus.Online)
activeDevices++;
else
offlineDevices++;
// Get active alerts
var alerts = await GetDeviceAlertsAsync(deviceId);
activeAlerts.AddRange(alerts);
}
// Calculate averages
overallEfficiency = efficiencyCount > 0 ? overallEfficiency / efficiencyCount : 0;
qualityRate = efficiencyCount > 0 ? qualityRate / efficiencyCount : 0;
return new DashboardSummary
{
GeneratedAt = DateTime.Now,
TotalDevices = totalDevices,
ActiveDevices = activeDevices,
OfflineDevices = offlineDevices,
TotalProductionToday = totalProductionToday,
TotalProductionThisWeek = totalProductionThisWeek,
TotalProductionThisMonth = totalProductionThisMonth,
OverallEfficiency = overallEfficiency,
QualityRate = qualityRate,
DeviceSummaries = deviceSummaries,
ActiveAlerts = activeAlerts
};
}
public async Task<OeeMetrics> CalculateOeeAsync(int deviceId, DateTime date)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null)
throw new KeyNotFoundException($"Device {deviceId} not found");
// Get planned production time (typically 8 hours or as configured)
var plannedProductionTime = await GetPlannedProductionTimeAsync(deviceId, date);
// Get actual production time
var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date);
var actualProductionTime = CalculateProductionTime(productionRecords);
// Calculate downtime
var downtime = plannedProductionTime - actualProductionTime;
// Get performance metrics
var totalPieces = productionRecords.Sum(r => r.Quantity);
var goodPieces = productionRecords.Where(r => r.IsGood).Sum(r => r.Quantity);
// Calculate OEE components
var availability = CalculateAvailability(plannedProductionTime, actualProductionTime);
var performance = await CalculatePerformanceAsync(deviceId, date);
var quality = totalPieces > 0 ? (goodPieces / totalPieces) * 100 : 0;
var oee = (availability * performance * quality) / 10000; // Convert percentage back to decimal
return new OeeMetrics
{
DeviceId = deviceId,
DeviceName = device.Name,
Date = date,
Availability = availability,
Performance = performance,
Quality = quality,
Oee = oee,
PlannedProductionTime = plannedProductionTime,
ActualProductionTime = actualProductionTime,
Downtime = downtime,
IdealCycleTime = await GetIdealCycleTimeAsync(deviceId),
TotalCycleTime = actualProductionTime,
TotalPieces = totalPieces,
GoodPieces = goodPieces
};
}
public async Task<ProductionForecast> GenerateProductionForecastAsync(ForecastFilter filter)
{
var device = await _deviceRepository.GetByIdAsync(filter.DeviceId);
if (device == null)
throw new KeyNotFoundException($"Device {filter.DeviceId} not found");
var historicalData = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(
filter.DeviceId, filter.HistoricalDataStart, filter.HistoricalDataEnd);
var dailyForecasts = new List<ForecastItem>();
var forecastAccuracy = 0.0m;
switch (filter.Model)
{
case ForecastModel.Linear:
dailyForecasts = GenerateLinearForecast(historicalData, filter.DaysToForecast);
break;
case ForecastModel.ExponentialSmoothing:
dailyForecasts = GenerateExponentialSmoothingForecast(historicalData, filter.DaysToForecast);
break;
case ForecastModel.MovingAverage:
dailyForecasts = GenerateMovingAverageForecast(historicalData, filter.DaysToForecast);
break;
default:
dailyForecasts = GenerateLinearForecast(historicalData, filter.DaysToForecast);
break;
}
// Calculate forecast accuracy if we have actual data
forecastAccuracy = CalculateForecastAccuracy(dailyForecasts);
return new ProductionForecast
{
DeviceId = filter.DeviceId,
DeviceName = device.Name,
ForecastStartDate = DateTime.Today,
ForecastEndDate = DateTime.Today.AddDays(filter.DaysToForecast - 1),
DailyForecasts = dailyForecasts,
ForecastAccuracy = forecastAccuracy,
ModelUsed = filter.Model
};
}
public async Task<AnomalyAnalysis> DetectProductionAnomaliesAsync(AnomalyFilter filter)
{
var deviceIds = filter.DeviceIds?.Any() == true ? filter.DeviceIds :
(await _deviceRepository.GetAllActiveDevicesAsync()).Select(d => d.Id).ToList();
var anomalies = new List<ProductionAnomaly>();
foreach (var deviceId in deviceIds)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null) continue;
var productionRecords = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(
deviceId, filter.StartDate, filter.EndDate);
// Detect production drop anomalies
if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.ProductionDrop)
{
var dropAnomalies = DetectProductionDropAnomalies(productionRecords);
anomalies.AddRange(dropAnomalies);
}
// Detect quality spike anomalies
if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.QualitySpike)
{
var qualityAnomalies = DetectQualitySpikeAnomalies(productionRecords);
anomalies.AddRange(qualityAnomalies);
}
// Detect downtime spike anomalies
if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.DowntimeSpike)
{
var downtimeAnomalies = DetectDowntimeSpikeAnomalies(deviceId, productionRecords);
anomalies.AddRange(downtimeAnomalies);
}
// Detect efficiency drop anomalies
if (!filter.Type.HasValue || filter.Type.Value == AnomalyType.EfficiencyDrop)
{
var efficiencyAnomalies = DetectEfficiencyDropAnomalies(productionRecords);
anomalies.AddRange(efficiencyAnomalies);
}
}
// Filter by severity
if (filter.MinSeverity.HasValue)
{
anomalies = anomalies.Where(a => a.Severity >= filter.MinSeverity.Value).ToList();
}
// Determine overall severity
var overallSeverity = DetermineOverallAnomalySeverity(anomalies);
return new AnomalyAnalysis
{
DeviceIds = deviceIds,
DeviceNames = deviceIds.Select(id => _deviceRepository.GetByIdAsync(id).Result?.Name ?? "Unknown").ToList(),
AnalysisStartDate = filter.StartDate,
AnalysisEndDate = filter.EndDate,
Anomalies = anomalies,
OverallSeverity = overallSeverity
};
}
#region Private Methods
private decimal CalculateLinearRegressionTrend(List<DailyProduction> dailyData)
{
if (dailyData.Count < 2) return 0;
int n = dailyData.Count;
decimal sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
for (int i = 0; i < n; i++)
{
sumX += i;
sumY += dailyData[i].Quantity;
sumXY += i * dailyData[i].Quantity;
sumXX += i * i;
}
// Linear regression: y = mx + b, where m is the slope
decimal numerator = n * sumXY - sumX * sumY;
decimal denominator = n * sumXX - sumX * sumX;
return denominator != 0 ? numerator / denominator : 0;
}
private ProductionTrendDirection DetermineTrendDirection(decimal trendCoefficient)
{
if (trendCoefficient > 0.1m)
return ProductionTrendDirection.Increasing;
else if (trendCoefficient < -0.1m)
return ProductionTrendDirection.Decreasing;
else if (Math.Abs(trendCoefficient) <= 0.1m && Math.Abs(trendCoefficient) > 0.01m)
return ProductionTrendDirection.Stable;
else
return ProductionTrendDirection.Volatile;
}
private async Task<decimal> GetProductionTargetAsync(int deviceId, DateTime date)
{
// This would typically come from system configuration or production planning
var systemConfig = await _systemRepository.GetSystemConfigurationAsync();
return systemConfig?.DailyProductionTarget ?? 100; // Default target
}
private async Task<decimal> CalculateQualityRateAsync(int deviceId, string programName, DateTime startDate, DateTime endDate)
{
var records = await _productionRepository.GetProductionRecordsByFilterAsync(new ReportFilter
{
DeviceIds = new List<int> { deviceId },
ProgramNames = programName != null ? new List<string> { programName } : null,
StartDate = startDate,
EndDate = endDate
});
var totalQuantity = records.Sum(r => r.Quantity);
var goodQuantity = records.Where(r => r.IsGood).Sum(r => r.Quantity);
return totalQuantity > 0 ? (goodQuantity / totalQuantity) * 100 : 0;
}
private TimeSpan CalculateRuntime(List<ProductionRecord> records)
{
if (records == null || records.Count == 0) return TimeSpan.Zero;
var startTime = records.Min(r => r.Created);
var endTime = records.Max(r => r.Created);
return endTime - startTime;
}
private async Task<TimeSpan> CalculateDowntimeAsync(int deviceId, DateTime startDate, DateTime endDate)
{
// Get device status changes
var statusRecords = await _collectionRepository.GetDeviceStatusHistoryAsync(deviceId, startDate, endDate);
// Calculate total downtime (time when device was not in production state)
var downtime = TimeSpan.Zero;
var previousTime = startDate;
foreach (var status in statusRecords.OrderBy(s => s.Timestamp))
{
if (status.Status != DeviceStatus.Running && status.Status != DeviceStatus.Idle)
{
downtime += status.Timestamp - previousTime;
}
previousTime = status.Timestamp;
}
// Add remaining time
downtime += endDate - previousTime;
return downtime;
}
private ReportMetadata CalculateReportMetadata(List<ProductionSummaryItem> summaryItems, ReportFilter filter)
{
return ReportMetadata.GeneratedAt; // Simplified for now
}
private async Task<EfficiencyMetrics> CalculateDeviceEfficiencyAsync(int deviceId, EfficiencyFilter filter)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null)
throw new KeyNotFoundException($"Device {deviceId} not found");
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(
deviceId, filter.StartDate, filter.EndDate);
var hourlyData = new List<HourlyEfficiency>();
var totalAvailability = 0m;
var totalPerformance = 0m;
var totalQuality = 0m;
var hourCount = 0;
// Group by hour if requested
if (filter.GroupBy == GroupBy.Hour)
{
var groupedByHour = records.GroupBy(r => r.Created.Date.AddHours(r.Created.Hour));
foreach (var hourGroup in groupedByHour)
{
var hour = hourGroup.Key;
var hourRecords = hourGroup.ToList();
// Calculate efficiency metrics for this hour
var availability = await CalculateHourlyAvailabilityAsync(deviceId, hour);
var performance = await CalculateHourlyPerformanceAsync(deviceId, hour);
var quality = CalculateHourlyQuality(hourRecords);
hourlyData.Add(new HourlyEfficiency
{
Hour = hour,
Availability = availability,
Performance = performance,
Quality = quality,
Oee = (availability * performance * quality) / 100
});
totalAvailability += availability;
totalPerformance += performance;
totalQuality += quality;
hourCount++;
}
}
// Calculate overall metrics
var availability = hourCount > 0 ? totalAvailability / hourCount : 0;
var performance = hourCount > 0 ? totalPerformance / hourCount : 0;
var quality = hourCount > 0 ? totalQuality / hourCount : 0;
var oee = (availability * performance * quality) / 100;
return new EfficiencyMetrics
{
DeviceId = deviceId,
DeviceName = device.Name,
PeriodStart = filter.StartDate,
PeriodEnd = filter.EndDate,
Availability = availability,
Performance = performance,
Quality = quality,
Oee = oee,
Utilization = DetermineUtilizationLevel(oee),
HourlyData = hourlyData
};
}
private EfficiencyMetrics AggregateEfficiencyMetrics(List<EfficiencyMetrics> metrics)
{
if (metrics == null || metrics.Count == 0)
return new EfficiencyMetrics();
var aggregated = new EfficiencyMetrics
{
DeviceIds = metrics.Select(m => m.DeviceId).ToList(),
PeriodStart = metrics.Min(m => m.PeriodStart),
PeriodEnd = metrics.Max(m => m.PeriodEnd),
Availability = metrics.Average(m => m.Availability),
Performance = metrics.Average(m => m.Performance),
Quality = metrics.Average(m => m.Quality),
Oee = metrics.Average(m => m.Oee),
HourlyData = metrics.SelectMany(m => m.HourlyData).ToList()
};
aggregated.Utilization = DetermineUtilizationLevel(aggregated.Oee);
return aggregated;
}
private EquipmentUtilization DetermineUtilizationLevel(decimal oee)
{
if (oee >= 85) return EquipmentUtilization.Optimal;
if (oee >= 70) return EquipmentUtilization.High;
if (oee >= 50) return EquipmentUtilization.Medium;
return EquipmentUtilization.Low;
}
private async Task<List<QualityMetric>> CalculateDeviceQualityMetricsAsync(int deviceId, QualityFilter filter)
{
// Simplified quality metrics calculation
var metrics = new List<QualityMetric>();
var defectRate = await CalculateQualityRateAsync(deviceId, null, filter.StartDate, filter.EndDate);
metrics.Add(new QualityMetric
{
MetricName = "Defect Rate",
Value = 100 - defectRate,
Unit = "%",
Timestamp = DateTime.Now
});
return metrics;
}
private async Task<List<DefectAnalysis>> AnalyzeDefectsAsync(int deviceId, QualityFilter filter)
{
// Simplified defect analysis
return new List<DefectAnalysis>();
}
private async Task<DeviceCurrentStatus> GetDeviceCurrentStatusAsync(int deviceId)
{
// This would typically get current status from real-time data
var records = await _collectionRepository.GetLatestDeviceStatusAsync(deviceId);
return records ?? new DeviceCurrentStatus();
}
private async Task<decimal> GetDeviceProductionForDateAsync(int deviceId, DateTime date)
{
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date);
return records.Sum(r => r.Quantity);
}
private async Task<decimal> GetDeviceProductionForDateRangeAsync(int deviceId, DateTime startDate, DateTime endDate)
{
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateRangeAsync(deviceId, startDate, endDate);
return records.Sum(r => r.Quantity);
}
private async Task<List<AlertSummary>> GetDeviceAlertsAsync(int deviceId)
{
var alerts = await _alarmRepository.GetActiveAlertsByDeviceAsync(deviceId);
return alerts.Select(a => new AlertSummary
{
AlertId = a.Id,
DeviceName = "Device " + deviceId, // Simplified
AlertType = (AlertType)a.AlertType,
Message = a.Message,
CreatedAt = a.CreatedAt,
IsActive = a.IsActive
}).ToList();
}
private async Task<TimeSpan> GetPlannedProductionTimeAsync(int deviceId, DateTime date)
{
// This would typically come from system configuration
var systemConfig = await _systemRepository.GetSystemConfigurationAsync();
return systemConfig?.DailyWorkingHours ?? TimeSpan.FromHours(8);
}
private TimeSpan CalculateProductionTime(List<ProductionRecord> records)
{
if (records == null || records.Count == 0) return TimeSpan.Zero;
var startTime = records.Min(r => r.Created);
var endTime = records.Max(r => r.Created);
return endTime - startTime;
}
private decimal CalculateAvailability(TimeSpan plannedTime, TimeSpan actualTime)
{
return plannedTime.TotalMinutes > 0 ?
(actualTime.TotalMinutes / plannedTime.TotalMinutes) * 100 : 0;
}
private async Task<decimal> CalculatePerformanceAsync(int deviceId, DateTime date)
{
// Simplified performance calculation
var records = await _productionRepository.GetProductionRecordsByDeviceAndDateAsync(deviceId, date);
var totalPieces = records.Sum(r => r.Quantity);
var idealCycleTime = await GetIdealCycleTimeAsync(deviceId);
var standardPieces = idealCycleTime > 0 ? (24 * 60) / idealCycleTime : 0; // Assuming 24 hours operation
return standardPieces > 0 ? (totalPieces / standardPieces) * 100 : 0;
}
private async Task<decimal> GetIdealCycleTimeAsync(int deviceId)
{
// This would typically come from device configuration
var device = await _deviceRepository.GetByIdAsync(deviceId);
return device?.IdealCycleTime ?? 1; // Default 1 minute per piece
}
private decimal CalculateHourlyQuality(List<ProductionRecord> hourRecords)
{
if (hourRecords == null || hourRecords.Count == 0) return 0;
var totalQuantity = hourRecords.Sum(r => r.Quantity);
var goodQuantity = hourRecords.Where(r => r.IsGood).Sum(r => r.Quantity);
return totalQuantity > 0 ? (goodQuantity / totalQuantity) * 100 : 0;
}
private async Task<decimal> CalculateHourlyAvailabilityAsync(int deviceId, DateTime hour)
{
// Simplified availability calculation
return 85m; // Default 85% availability
}
private async Task<decimal> CalculateHourlyPerformanceAsync(int deviceId, DateTime hour)
{
// Simplified performance calculation
return 90m; // Default 90% performance
}
private List<ForecastItem> GenerateLinearForecast(List<ProductionRecord> historicalData, int daysToForecast)
{
var forecasts = new List<ForecastItem>();
var lastDate = historicalData.Max(r => r.Created);
// Calculate average daily production
var avgDailyProduction = historicalData.Average(r => r.Quantity);
for (int i = 1; i <= daysToForecast; i++)
{
var forecastDate = lastDate.AddDays(i);
var forecastQuantity = avgDailyProduction; // Simple average
forecasts.Add(new ForecastItem
{
Date = forecastDate,
ForecastedQuantity = forecastQuantity,
ConfidenceLower = forecastQuantity * 0.8m,
ConfidenceUpper = forecastQuantity * 1.2m,
ActualQuantity = 0, // Will be updated when actual data is available
Variance = 0
});
}
return forecasts;
}
private List<ForecastItem> GenerateExponentialSmoothingForecast(List<ProductionRecord> historicalData, int daysToForecast)
{
// Simplified exponential smoothing
var alpha = 0.3m; // Smoothing factor
var forecasts = new List<ForecastItem>();
if (historicalData.Count == 0) return forecasts;
var lastSmoothed = historicalData.First().Quantity;
var lastDate = historicalData.Max(r => r.Created);
for (int i = 1; i <= daysToForecast; i++)
{
var forecastDate = lastDate.AddDays(i);
var forecastQuantity = lastSmoothed;
forecasts.Add(new ForecastItem
{
Date = forecastDate,
ForecastedQuantity = forecastQuantity,
ConfidenceLower = forecastQuantity * 0.85m,
ConfidenceUpper = forecastQuantity * 1.15m,
ActualQuantity = 0,
Variance = 0
});
lastSmoothed = alpha * forecastQuantity + (1 - alpha) * lastSmoothed;
}
return forecasts;
}
private List<ForecastItem> GenerateMovingAverageForecast(List<ProductionRecord> historicalData, int daysToForecast)
{
var forecasts = new List<ForecastItem>();
var windowSize = Math.Min(7, historicalData.Count); // 7-day moving average
if (historicalData.Count < windowSize) return forecasts;
var lastDate = historicalData.Max(r => r.Created);
// Calculate moving average
var movingAvg = historicalData.TakeLast(windowSize).Average(r => r.Quantity);
for (int i = 1; i <= daysToForecast; i++)
{
var forecastDate = lastDate.AddDays(i);
var forecastQuantity = movingAvg;
forecasts.Add(new ForecastItem
{
Date = forecastDate,
ForecastedQuantity = forecastQuantity,
ConfidenceLower = forecastQuantity * 0.9m,
ConfidenceUpper = forecastQuantity * 1.1m,
ActualQuantity = 0,
Variance = 0
});
}
return forecasts;
}
private decimal CalculateForecastAccuracy(List<ForecastItem> forecasts)
{
// Simplified accuracy calculation based on confidence intervals
var accurateForecasts = forecasts.Count(f => f.ForecastedQuantity >= f.ConfidenceLower &&
f.ForecastedQuantity <= f.ConfidenceUpper);
return forecasts.Count > 0 ? (accurateForecasts / forecasts.Count) * 100 : 0;
}
private List<ProductionAnomaly> DetectProductionDropAnomalies(List<ProductionRecord> records)
{
var anomalies = new List<ProductionAnomaly>();
if (records.Count < 2) return anomalies;
// Compare each day with the previous day
var groupedByDay = records.GroupBy(r => r.Created.Date).OrderBy(g => g.Key);
var previousDay = groupedByDay.FirstOrDefault();
foreach (var currentDay in groupedByDay.Skip(1))
{
var previousQuantity = previousDay.Sum(r => r.Quantity);
var currentQuantity = currentDay.Sum(r => r.Quantity);
if (previousQuantity > 0)
{
var dropPercentage = (decimal)((previousQuantity - currentQuantity) / (double)previousQuantity * 100);
if (dropPercentage > 30) // 30% drop threshold
{
anomalies.Add(new ProductionAnomaly
{
Timestamp = currentDay.Key,
Type = AnomalyType.ProductionDrop,
Severity = dropPercentage > 50 ? AnomalySeverity.Critical : AnomalySeverity.High,
Deviation = dropPercentage,
Description = $"Production dropped by {dropPercentage:F1}% from previous day",
RecommendedAction = dropPercentage > 50 ? AnomalyAction.Shutdown : AnomalyAction.Investigate
});
}
}
previousDay = currentDay;
}
return anomalies;
}
private List<ProductionAnomaly> DetectQualitySpikeAnomalies(List<ProductionRecord> records)
{
var anomalies = new List<ProductionAnomaly>();
if (records.Count == 0) return anomalies;
var qualityRates = records.GroupBy(r => r.Created.Date)
.Select(g => new
{
Date = g.Key,
QualityRate = g.Where(r => r.IsGood).Average(r => (decimal)r.Quantity / (decimal)g.Sum(r => r.Quantity)) * 100
});
var avgQuality = qualityRates.Average(q => q.QualityRate);
foreach (var day in qualityRates)
{
var deviation = Math.Abs((decimal)(day.QualityRate - avgQuality));
if (deviation > 20 && day.QualityRate > avgQuality) // Significant quality improvement
{
anomalies.Add(new ProductionAnomaly
{
Timestamp = day.Date,
Type = AnomalyType.QualitySpike,
Severity = deviation > 30 ? AnomalySeverity.High : AnomalySeverity.Medium,
Deviation = deviation,
Description = $"Quality rate spiked to {day.QualityRate:F1}%",
RecommendedAction = AnomalyAction.Monitor
});
}
}
return anomalies;
}
private List<ProductionAnomaly> DetectDowntimeSpikeAnomalies(int deviceId, List<ProductionRecord> records)
{
var anomalies = new List<ProductionAnomaly>();
// Simplified downtime spike detection
var downtimePeriods = CalculateDowntimePeriods(records);
var avgDowntime = downtimePeriods.Average(d => d.Duration.TotalMinutes);
foreach (var period in downtimePeriods)
{
if (period.Duration.TotalMinutes > avgDowntime * 2) // Double the average downtime
{
anomalies.Add(new ProductionAnomaly
{
Timestamp = period.Start,
Type = AnomalyType.DowntimeSpike,
Severity = period.Duration.TotalMinutes > 120 ? AnomalySeverity.Critical : AnomalySeverity.High,
Deviation = (decimal)(period.Duration.TotalMinutes / avgDowntime),
Description = $"Extended downtime period: {period.Duration.TotalMinutes:F0} minutes",
RecommendedAction = period.Duration.TotalMinutes > 120 ? AnomalyAction.Shutdown : AnomalyAction.Alert
});
}
}
return anomalies;
}
private List<ProductionAnomaly> DetectEfficiencyDropAnomalies(List<ProductionRecord> records)
{
var anomalies = new List<ProductionAnomaly>();
// Simplified efficiency drop detection
var efficiencies = records.GroupBy(r => r.Created.Date)
.Select(g => new
{
Date = g.Key,
Efficiency = g.Sum(r => r.Quantity) / g.Sum(r => r.TargetQuantity) * 100
});
var avgEfficiency = efficiencies.Average(e => e.Efficiency);
foreach (var day in efficiencies)
{
if (day.Efficiency < avgEfficiency * 0.7) // 30% below average
{
anomalies.Add(new ProductionAnomaly
{
Timestamp = day.Date,
Type = AnomalyType.EfficiencyDrop,
Severity = day.Efficiency < avgEfficiency * 0.5 ? AnomalySeverity.Critical : AnomalySeverity.High,
Deviation = avgEfficiency - day.Efficiency,
Description = $"Efficiency dropped to {day.Efficiency:F1}%",
RecommendedAction = AnomalyAction.Investigate
});
}
}
return anomalies;
}
private List<DowntimePeriod> CalculateDowntimePeriods(List<ProductionRecord> records)
{
var downtimePeriods = new List<DowntimePeriod>();
if (records.Count == 0) return downtimePeriods;
var sortedRecords = records.OrderBy(r => r.Created).ToList();
var downtimeStart = sortedRecords.First().Created;
foreach (var record in sortedRecords)
{
// Assume there's downtime if there's a gap of more than 5 minutes between records
if (record.Created - downtimeStart > TimeSpan.FromMinutes(5))
{
downtimePeriods.Add(new DowntimePeriod
{
Start = downtimeStart,
End = record.Created,
Duration = record.Created - downtimeStart
});
downtimeStart = record.Created;
}
}
return downtimePeriods;
}
private AnomalySeverity DetermineOverallAnomalySeverity(List<ProductionAnomaly> anomalies)
{
if (anomalies.Count == 0) return AnomalySeverity.Low;
var maxSeverity = anomalies.Max(a => a.Severity);
var criticalCount = anomalies.Count(a => a.Severity == AnomalySeverity.Critical);
if (criticalCount > 0) return AnomalySeverity.Critical;
if (maxSeverity >= AnomalySeverity.High) return AnomalySeverity.High;
if (maxSeverity >= AnomalySeverity.Medium) return AnomalySeverity.Medium;
return AnomalySeverity.Low;
}
#endregion
}
// Helper class for downtime periods
internal class DowntimePeriod
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public TimeSpan Duration { get; set; }
}
}