version 2
This commit is contained in:
229
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
229
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
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} یافت نشد");
|
||||
}
|
||||
|
||||
// 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 question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند:
|
||||
|
||||
{dataBuilder}
|
||||
|
||||
لطفاً یک تحلیل خلاصه و کاربردی از این دادهها بده که شامل موارد زیر باشه:
|
||||
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||||
2. روندهای مشاهده شده در طول روز
|
||||
3. هر گونه نکته یا هشدار مهم
|
||||
4. پیشنهادات برای بهبود شرایط گلخانه
|
||||
|
||||
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
ChatResponse? aiResponse;
|
||||
|
||||
try
|
||||
{
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی." },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user