480 lines
20 KiB
C#
480 lines
20 KiB
C#
using System.Diagnostics;
|
||
using System.Text;
|
||
using GreenHome.AI.DeepSeek;
|
||
using GreenHome.Application;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace GreenHome.Infrastructure;
|
||
|
||
public class DailyReportService : IDailyReportService
|
||
{
|
||
private readonly GreenHomeDbContext _context;
|
||
private readonly IDeepSeekService _deepSeekService;
|
||
private readonly ILogger<DailyReportService> _logger;
|
||
|
||
public DailyReportService(
|
||
GreenHomeDbContext context,
|
||
IDeepSeekService deepSeekService,
|
||
ILogger<DailyReportService> logger)
|
||
{
|
||
_context = context;
|
||
_deepSeekService = deepSeekService;
|
||
_logger = logger;
|
||
}
|
||
|
||
public async Task<DailyReportResponse> GetOrCreateDailyReportAsync(
|
||
DailyReportRequest request,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// Validate Persian date format
|
||
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
|
||
{
|
||
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد", nameof(request.PersianDate));
|
||
}
|
||
|
||
// Check if report already exists
|
||
var existingReport = await _context.DailyReports
|
||
.Include(r => r.Device)
|
||
.FirstOrDefaultAsync(
|
||
r => r.DeviceId == request.DeviceId && r.PersianDate == request.PersianDate,
|
||
cancellationToken);
|
||
|
||
if (existingReport != null)
|
||
{
|
||
_logger.LogInformation(
|
||
"گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date} از قبل موجود است",
|
||
request.DeviceId, request.PersianDate);
|
||
|
||
return new DailyReportResponse
|
||
{
|
||
Id = existingReport.Id,
|
||
DeviceId = existingReport.DeviceId,
|
||
DeviceName = existingReport.Device?.DeviceName ?? string.Empty,
|
||
PersianDate = existingReport.PersianDate,
|
||
Analysis = existingReport.Analysis,
|
||
RecordCount = existingReport.RecordCount,
|
||
SampledRecordCount = existingReport.SampledRecordCount,
|
||
TotalTokens = existingReport.TotalTokens,
|
||
CreatedAt = existingReport.CreatedAt,
|
||
FromCache = true
|
||
};
|
||
}
|
||
|
||
// Get device info
|
||
var device = await _context.Devices
|
||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||
|
||
if (device == null)
|
||
{
|
||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||
}
|
||
|
||
// Get device settings (including ProductType if available)
|
||
var deviceSettings = await _context.DeviceSettings
|
||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||
|
||
// Query telemetry data for the specified date
|
||
var telemetryRecords = await _context.TelemetryRecords
|
||
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
|
||
.OrderBy(t => t.TimestampUtc)
|
||
.Select(t => new
|
||
{
|
||
t.TimestampUtc,
|
||
t.TemperatureC,
|
||
t.HumidityPercent,
|
||
t.Lux,
|
||
t.GasPPM
|
||
})
|
||
.ToListAsync(cancellationToken);
|
||
|
||
if (telemetryRecords.Count == 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"هیچ رکوردی برای دستگاه {request.DeviceId} در تاریخ {request.PersianDate} یافت نشد");
|
||
}
|
||
|
||
// Sample records: take first record from every 20 records
|
||
var sampledRecords = telemetryRecords
|
||
.Select((record, index) => new { record, index })
|
||
.Where(x => x.index % 20 == 0)
|
||
.Select(x => x.record)
|
||
.ToList();
|
||
|
||
_logger.LogInformation(
|
||
"تعداد {TotalCount} رکورد یافت شد. نمونهبرداری: {SampledCount} رکورد",
|
||
telemetryRecords.Count, sampledRecords.Count);
|
||
|
||
// Build the data string for AI
|
||
var dataBuilder = new StringBuilder();
|
||
dataBuilder.AppendLine("زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
|
||
dataBuilder.AppendLine("------|----------|-----------|-----------|----------");
|
||
|
||
foreach (var record in sampledRecords)
|
||
{
|
||
// Convert UTC to local time for display
|
||
var localTime = record.TimestampUtc.AddHours(3.5); // Iran timezone (UTC+3:30)
|
||
dataBuilder.AppendLine(
|
||
$"{localTime:HH:mm:ss} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
|
||
}
|
||
|
||
// Prepare the question for AI
|
||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||
: string.Empty;
|
||
|
||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||
: string.Empty;
|
||
|
||
var question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||
|
||
{dataBuilder}
|
||
|
||
لطفاً یک تحلیل خلاصه و کاربردی از این دادهها بده که شامل موارد زیر باشه:
|
||
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||
2. روندهای مشاهده شده در طول روز
|
||
3. هر گونه نکته یا هشدار مهم
|
||
4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
|
||
|
||
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||
|
||
// Send to DeepSeek
|
||
var stopwatch = Stopwatch.StartNew();
|
||
ChatResponse? aiResponse;
|
||
|
||
try
|
||
{
|
||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||
|
||
var chatRequest = new ChatRequest
|
||
{
|
||
Model = "deepseek-chat",
|
||
Messages = new List<ChatMessage>
|
||
{
|
||
new() { Role = "system", Content = systemMessage },
|
||
new() { Role = "user", Content = question }
|
||
},
|
||
Temperature = 0.7
|
||
};
|
||
|
||
aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "خطا در فراخوانی DeepSeek API");
|
||
throw new InvalidOperationException("خطا در دریافت تحلیل از سرویس هوش مصنوعی", ex);
|
||
}
|
||
|
||
stopwatch.Stop();
|
||
|
||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||
{
|
||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||
}
|
||
|
||
var analysis = aiResponse.Choices[0].Message!.Content;
|
||
|
||
// Save the report
|
||
var dailyReport = new Domain.DailyReport
|
||
{
|
||
DeviceId = request.DeviceId,
|
||
PersianDate = request.PersianDate,
|
||
PersianYear = year,
|
||
PersianMonth = month,
|
||
PersianDay = day,
|
||
Analysis = analysis,
|
||
RecordCount = telemetryRecords.Count,
|
||
SampledRecordCount = sampledRecords.Count,
|
||
PromptTokens = aiResponse.Usage?.PromptTokens ?? 0,
|
||
CompletionTokens = aiResponse.Usage?.CompletionTokens ?? 0,
|
||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||
Model = aiResponse.Model,
|
||
ResponseTimeMs = stopwatch.ElapsedMilliseconds,
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
|
||
_context.DailyReports.Add(dailyReport);
|
||
await _context.SaveChangesAsync(cancellationToken);
|
||
|
||
_logger.LogInformation(
|
||
"گزارش روزانه جدید برای دستگاه {DeviceId} و تاریخ {Date} ایجاد شد. توکن مصرف شده: {Tokens}",
|
||
request.DeviceId, request.PersianDate, dailyReport.TotalTokens);
|
||
|
||
return new DailyReportResponse
|
||
{
|
||
Id = dailyReport.Id,
|
||
DeviceId = dailyReport.DeviceId,
|
||
DeviceName = device.DeviceName,
|
||
PersianDate = dailyReport.PersianDate,
|
||
Analysis = dailyReport.Analysis,
|
||
RecordCount = dailyReport.RecordCount,
|
||
SampledRecordCount = dailyReport.SampledRecordCount,
|
||
TotalTokens = dailyReport.TotalTokens,
|
||
CreatedAt = dailyReport.CreatedAt,
|
||
FromCache = false
|
||
};
|
||
}
|
||
|
||
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
|
||
{
|
||
year = month = day = 0;
|
||
|
||
if (string.IsNullOrWhiteSpace(persianDate))
|
||
return false;
|
||
|
||
var parts = persianDate.Split('/');
|
||
if (parts.Length != 3)
|
||
return false;
|
||
|
||
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
|
||
return false;
|
||
|
||
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
|
||
return false;
|
||
|
||
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
public async Task<DailyReportResponse> GetWeeklyAnalysisAsync(
|
||
WeeklyAnalysisRequest request,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// Get device info
|
||
var device = await _context.Devices
|
||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||
|
||
if (device == null)
|
||
{
|
||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||
}
|
||
|
||
// Get device settings
|
||
var deviceSettings = await _context.DeviceSettings
|
||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||
|
||
// Query telemetry data for the week
|
||
var telemetryRecords = await _context.TelemetryRecords
|
||
.Where(t => t.DeviceId == request.DeviceId &&
|
||
string.Compare(t.PersianDate, request.StartDate) >= 0 &&
|
||
string.Compare(t.PersianDate, request.EndDate) <= 0)
|
||
.OrderBy(t => t.TimestampUtc)
|
||
.Select(t => new
|
||
{
|
||
t.TimestampUtc,
|
||
t.TemperatureC,
|
||
t.HumidityPercent,
|
||
t.Lux,
|
||
t.GasPPM,
|
||
t.PersianDate
|
||
})
|
||
.ToListAsync(cancellationToken);
|
||
|
||
if (telemetryRecords.Count == 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد");
|
||
}
|
||
|
||
// Sample 1 per 100 records
|
||
var sampledRecords = telemetryRecords
|
||
.Select((record, index) => new { record, index })
|
||
.Where(x => x.index % 100 == 0)
|
||
.Select(x => x.record)
|
||
.ToList();
|
||
|
||
_logger.LogInformation(
|
||
"تعداد {TotalCount} رکورد یافت شد. نمونهبرداری هفتگی: {SampledCount} رکورد",
|
||
telemetryRecords.Count, sampledRecords.Count);
|
||
|
||
// Build the data string
|
||
var dataBuilder = new StringBuilder();
|
||
dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
|
||
dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------");
|
||
|
||
foreach (var record in sampledRecords)
|
||
{
|
||
var localTime = record.TimestampUtc.AddHours(3.5);
|
||
dataBuilder.AppendLine(
|
||
$"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
|
||
}
|
||
|
||
// Prepare AI prompt
|
||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||
: string.Empty;
|
||
|
||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||
: string.Empty;
|
||
|
||
var question = $@"این دادههای تلمتری یک هفته ({request.StartDate} تا {request.EndDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||
|
||
{dataBuilder}
|
||
|
||
لطفاً یک تحلیل جامع هفتگی بده که شامل:
|
||
1. خلاصه روند هفتگی دما، رطوبت، نور و کیفیت هوا
|
||
2. مقایسه شرایط در روزهای مختلف هفته
|
||
3. نکات و هشدارهای مهم
|
||
4. توصیهها برای هفته آینده
|
||
|
||
خلاصه و کاربردی باش (حداکثر 500 کلمه).";
|
||
|
||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||
|
||
// Send to DeepSeek
|
||
var stopwatch = Stopwatch.StartNew();
|
||
var chatRequest = new ChatRequest
|
||
{
|
||
Model = "deepseek-chat",
|
||
Messages = new List<ChatMessage>
|
||
{
|
||
new() { Role = "system", Content = systemMessage },
|
||
new() { Role = "user", Content = question }
|
||
},
|
||
Temperature = 0.7
|
||
};
|
||
|
||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||
stopwatch.Stop();
|
||
|
||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||
{
|
||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||
}
|
||
|
||
return new DailyReportResponse
|
||
{
|
||
Id = 0,
|
||
DeviceId = request.DeviceId,
|
||
DeviceName = device.DeviceName,
|
||
PersianDate = $"{request.StartDate} تا {request.EndDate}",
|
||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||
RecordCount = telemetryRecords.Count,
|
||
SampledRecordCount = sampledRecords.Count,
|
||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||
CreatedAt = DateTime.UtcNow,
|
||
FromCache = false
|
||
};
|
||
}
|
||
|
||
public async Task<DailyReportResponse> GetMonthlyAnalysisAsync(
|
||
MonthlyAnalysisRequest request,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// Get device info
|
||
var device = await _context.Devices
|
||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||
|
||
if (device == null)
|
||
{
|
||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||
}
|
||
|
||
// Get device settings
|
||
var deviceSettings = await _context.DeviceSettings
|
||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||
|
||
// Get all daily reports for this month
|
||
var dailyReports = await _context.DailyReports
|
||
.Where(dr => dr.DeviceId == request.DeviceId &&
|
||
dr.PersianYear == request.Year &&
|
||
dr.PersianMonth == request.Month)
|
||
.OrderBy(dr => dr.PersianDay)
|
||
.Select(dr => new { dr.PersianDate, dr.Analysis })
|
||
.ToListAsync(cancellationToken);
|
||
|
||
if (dailyReports.Count == 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"هیچ تحلیل روزانهای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد");
|
||
}
|
||
|
||
// Build summary of daily analyses
|
||
var summaryBuilder = new StringBuilder();
|
||
summaryBuilder.AppendLine($"تحلیلهای روزانه ماه {request.Month} سال {request.Year}:");
|
||
summaryBuilder.AppendLine();
|
||
|
||
foreach (var report in dailyReports)
|
||
{
|
||
summaryBuilder.AppendLine($"📅 {report.PersianDate}:");
|
||
summaryBuilder.AppendLine(report.Analysis);
|
||
summaryBuilder.AppendLine();
|
||
summaryBuilder.AppendLine("---");
|
||
summaryBuilder.AppendLine();
|
||
}
|
||
|
||
// Prepare AI prompt
|
||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||
: string.Empty;
|
||
|
||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||
: string.Empty;
|
||
|
||
var question = $@"این تحلیلهای روزانه یک ماه ({request.Month}/{request.Year}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||
|
||
{summaryBuilder}
|
||
|
||
لطفاً یک تحلیل جامع ماهانه بده که شامل:
|
||
1. خلاصه کلی عملکرد ماه
|
||
2. روندهای اصلی و تغییرات مهم
|
||
3. نقاط قوت و ضعف
|
||
4. توصیههای کلیدی برای ماه آینده
|
||
5. نکات مهم برای بهبود بهرهوری
|
||
|
||
جامع و کاربردی باش (حداکثر 800 کلمه).";
|
||
|
||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده."
|
||
: "تو یک متخصص کشاورزی و گلخانه هستی. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده.";
|
||
|
||
// Send to DeepSeek
|
||
var stopwatch = Stopwatch.StartNew();
|
||
var chatRequest = new ChatRequest
|
||
{
|
||
Model = "deepseek-chat",
|
||
Messages = new List<ChatMessage>
|
||
{
|
||
new() { Role = "system", Content = systemMessage },
|
||
new() { Role = "user", Content = question }
|
||
},
|
||
Temperature = 0.7
|
||
};
|
||
|
||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||
stopwatch.Stop();
|
||
|
||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||
{
|
||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||
}
|
||
|
||
return new DailyReportResponse
|
||
{
|
||
Id = 0,
|
||
DeviceId = request.DeviceId,
|
||
DeviceName = device.DeviceName,
|
||
PersianDate = $"ماه {request.Month} سال {request.Year}",
|
||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||
RecordCount = dailyReports.Count,
|
||
SampledRecordCount = dailyReports.Count,
|
||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||
CreatedAt = DateTime.UtcNow,
|
||
FromCache = false
|
||
};
|
||
}
|
||
}
|
||
|