version 2

This commit is contained in:
2025-12-16 16:52:40 +03:30
parent 61e86b1e96
commit 139924db94
52 changed files with 7350 additions and 321 deletions

View File

@@ -0,0 +1,126 @@
using GreenHome.Application;
using GreenHome.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace GreenHome.Infrastructure;
public sealed class AIQueryService : IAIQueryService
{
private readonly GreenHomeDbContext dbContext;
private readonly ILogger<AIQueryService> logger;
public AIQueryService(
GreenHomeDbContext dbContext,
ILogger<AIQueryService> logger)
{
this.dbContext = dbContext;
this.logger = logger;
}
public async Task<AIQuery> SaveQueryAsync(AIQuery query, CancellationToken cancellationToken = default)
{
try
{
dbContext.AIQueries.Add(query);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AI query saved: {QueryId}, Tokens: {TotalTokens}",
query.Id, query.TotalTokens);
return query;
}
catch (Exception ex)
{
logger.LogError(ex, "Error saving AI query");
throw;
}
}
public async Task<List<AIQuery>> GetDeviceQueriesAsync(
int deviceId,
int take = 50,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
.Where(q => q.DeviceId == deviceId)
.OrderByDescending(q => q.CreatedAt)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<List<AIQuery>> GetUserQueriesAsync(
int userId,
int take = 50,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
.Where(q => q.UserId == userId)
.OrderByDescending(q => q.CreatedAt)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> GetDeviceTotalTokensAsync(
int deviceId,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
.Where(q => q.DeviceId == deviceId)
.SumAsync(q => q.TotalTokens, cancellationToken);
}
public async Task<int> GetUserTotalTokensAsync(
int userId,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
.Where(q => q.UserId == userId)
.SumAsync(q => q.TotalTokens, cancellationToken);
}
public async Task<List<AIQuery>> GetRecentQueriesAsync(
int take = 20,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
.Include(q => q.Device)
.Include(q => q.User)
.OrderByDescending(q => q.CreatedAt)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<AIQueryStats> GetStatsAsync(CancellationToken cancellationToken = default)
{
var today = DateTime.UtcNow.Date;
var allQueries = await dbContext.AIQueries
.Select(q => new
{
q.TotalTokens,
q.PromptTokens,
q.CompletionTokens,
q.ResponseTimeMs,
q.CreatedAt
})
.ToListAsync(cancellationToken);
var todayQueries = allQueries.Where(q => q.CreatedAt >= today).ToList();
return new AIQueryStats
{
TotalQueries = allQueries.Count,
TotalTokensUsed = allQueries.Sum(q => q.TotalTokens),
TotalPromptTokens = allQueries.Sum(q => q.PromptTokens),
TotalCompletionTokens = allQueries.Sum(q => q.CompletionTokens),
AverageResponseTimeMs = allQueries.Any()
? allQueries.Where(q => q.ResponseTimeMs.HasValue)
.Average(q => q.ResponseTimeMs ?? 0)
: 0,
TodayQueries = todayQueries.Count,
TodayTokens = todayQueries.Sum(q => q.TotalTokens)
};
}
}

View File

@@ -0,0 +1,131 @@
using AutoMapper;
using GreenHome.Application;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
public sealed class AlertConditionService : IAlertConditionService
{
private readonly GreenHomeDbContext dbContext;
private readonly IMapper mapper;
public AlertConditionService(GreenHomeDbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
}
public async Task<IReadOnlyList<AlertConditionDto>> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
{
var conditions = await dbContext.AlertConditions
.Include(x => x.Device)
.Include(x => x.Rules)
.Where(x => x.DeviceId == deviceId)
.OrderByDescending(x => x.CreatedAt)
.AsNoTracking()
.ToListAsync(cancellationToken);
return mapper.Map<List<AlertConditionDto>>(conditions);
}
public async Task<AlertConditionDto?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
.Include(x => x.Device)
.Include(x => x.Rules)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
return condition != null ? mapper.Map<AlertConditionDto>(condition) : null;
}
public async Task<int> CreateAsync(CreateAlertConditionRequest request, CancellationToken cancellationToken)
{
var condition = new Domain.AlertCondition
{
DeviceId = request.DeviceId,
NotificationType = request.NotificationType,
TimeType = request.TimeType,
CallCooldownMinutes = request.CallCooldownMinutes,
SmsCooldownMinutes = request.SmsCooldownMinutes,
IsEnabled = request.IsEnabled,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
// Add rules
var rules = mapper.Map<List<Domain.AlertRule>>(request.Rules);
foreach (var rule in rules)
{
condition.Rules.Add(rule);
}
dbContext.AlertConditions.Add(condition);
await dbContext.SaveChangesAsync(cancellationToken);
return condition.Id;
}
public async Task UpdateAsync(UpdateAlertConditionRequest request, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
.Include(x => x.Rules)
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (condition == null)
{
throw new InvalidOperationException($"Alert condition not found: {request.Id}");
}
condition.NotificationType = request.NotificationType;
condition.TimeType = request.TimeType;
condition.CallCooldownMinutes = request.CallCooldownMinutes;
condition.SmsCooldownMinutes = request.SmsCooldownMinutes;
condition.IsEnabled = request.IsEnabled;
condition.UpdatedAt = DateTime.UtcNow;
// Remove old rules and add new ones
dbContext.AlertRules.RemoveRange(condition.Rules);
condition.Rules.Clear();
var newRules = mapper.Map<List<Domain.AlertRule>>(request.Rules);
foreach (var rule in newRules)
{
condition.Rules.Add(rule);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(int id, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (condition == null)
{
throw new InvalidOperationException($"Alert condition not found: {id}");
}
dbContext.AlertConditions.Remove(condition);
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<bool> ToggleEnabledAsync(int id, bool isEnabled, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (condition == null)
{
return false;
}
condition.IsEnabled = isEnabled;
condition.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -1,5 +1,6 @@
using GreenHome.Application;
using GreenHome.Sms.Ippanel;
using GreenHome.VoiceCall.Avanak;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Text.Json;
@@ -12,280 +13,153 @@ public sealed class AlertService : IAlertService
private readonly GreenHomeDbContext dbContext;
private readonly IDeviceSettingsService deviceSettingsService;
private readonly ISmsService smsService;
private readonly IVoiceCallService voiceCallService;
private readonly ISunCalculatorService sunCalculatorService;
private readonly ILogger<AlertService> logger;
private const int AlertCooldownMinutes = 10;
private sealed record AlertInfo(
string Type,
string Message,
string ParameterName,
decimal Value,
string Status
);
public AlertService(
GreenHomeDbContext dbContext,
IDeviceSettingsService deviceSettingsService,
ISmsService smsService,
IVoiceCallService voiceCallService,
ISunCalculatorService sunCalculatorService,
ILogger<AlertService> logger)
{
this.dbContext = dbContext;
this.deviceSettingsService = deviceSettingsService;
this.smsService = smsService;
this.voiceCallService = voiceCallService;
this.sunCalculatorService = sunCalculatorService;
this.logger = logger;
}
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
{
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
if (settings == null)
{
return;
}
// Get device with settings and user
var device = await dbContext.Devices
.Include(d => d.User)
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
if (device == null || device.User == null)
{
logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId);
return;
}
var alerts = CollectAlerts(telemetry, settings, device.DeviceName);
// Get device settings for location
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
foreach (var alert in alerts)
// Get all enabled alert conditions for this device
var conditions = await dbContext.AlertConditions
.Include(c => c.Rules)
.Where(c => c.DeviceId == deviceId && c.IsEnabled)
.ToListAsync(cancellationToken);
if (!conditions.Any())
{
await SendAlertIfNeededAsync(deviceId, device.User.Id, device.DeviceName, alert, cancellationToken);
logger.LogDebug("No enabled alert conditions for device: DeviceId={DeviceId}", deviceId);
return;
}
// Determine if it's daytime or nighttime
bool? isDaytime = null;
if (settings?.Latitude != null && settings.Longitude != null)
{
isDaytime = sunCalculatorService.IsDaytime(DateTime.UtcNow, settings.Latitude.Value, settings.Longitude.Value);
}
// Check each condition
foreach (var condition in conditions)
{
// Check time type filter
if (condition.TimeType == Domain.AlertTimeType.Day && isDaytime == false)
{
continue; // This condition is for daytime only, but it's nighttime
}
if (condition.TimeType == Domain.AlertTimeType.Night && isDaytime == true)
{
continue; // This condition is for nighttime only, but it's daytime
}
// Check if all rules match (AND logic)
var allRulesMatch = condition.Rules.All(rule => CheckRule(rule, telemetry));
if (allRulesMatch && condition.Rules.Any())
{
// All rules passed, send alert if cooldown period has passed
await SendAlertForConditionAsync(condition, device, telemetry, cancellationToken);
}
}
}
private List<AlertInfo> CollectAlerts(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName)
private bool CheckRule(Domain.AlertRule rule, TelemetryDto telemetry)
{
var alerts = new List<AlertInfo>();
// Get sensor value
var sensorValue = rule.SensorType switch
{
Domain.SensorType.Temperature => telemetry.TemperatureC,
Domain.SensorType.Humidity => telemetry.HumidityPercent,
Domain.SensorType.Soil => telemetry.SoilPercent,
Domain.SensorType.Gas => telemetry.GasPPM,
Domain.SensorType.Lux => telemetry.Lux,
_ => 0m
};
CheckTemperatureAlert(telemetry, settings, deviceName, alerts);
CheckHumidityAlert(telemetry, settings, deviceName, alerts);
CheckSoilAlert(telemetry, deviceName, alerts);
CheckGasAlert(telemetry, settings, deviceName, alerts);
CheckLuxAlert(telemetry, settings, deviceName, alerts);
return alerts;
// Check comparison
return rule.ComparisonType switch
{
Domain.ComparisonType.GreaterThan => sensorValue > rule.Value1,
Domain.ComparisonType.LessThan => sensorValue < rule.Value1,
Domain.ComparisonType.Between => rule.Value2 != null && sensorValue >= rule.Value1 && sensorValue <= rule.Value2.Value,
Domain.ComparisonType.OutOfRange => rule.Value2 != null && (sensorValue < rule.Value1 || sensorValue > rule.Value2.Value),
_ => false
};
}
private void CheckTemperatureAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
{
if (telemetry.TemperatureC > settings.MaxTemperature)
{
alerts.Add(new AlertInfo(
Type: "Temperature",
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداکثر مجاز ({settings.MaxTemperature}) بیشتر است.",
ParameterName: "دما",
Value: telemetry.TemperatureC,
Status: "بالاتر"
));
}
else if (telemetry.TemperatureC < settings.MinTemperature)
{
alerts.Add(new AlertInfo(
Type: "Temperature",
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداقل مجاز ({settings.MinTemperature}) کمتر است.",
ParameterName: "دما",
Value: telemetry.TemperatureC,
Status: "پایین‌تر"
));
}
}
private void CheckHumidityAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
{
if (telemetry.HumidityPercent > settings.MaxHumidityPercent)
{
alerts.Add(new AlertInfo(
Type: "Humidity",
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداکثر مجاز ({settings.MaxHumidityPercent}%) بیشتر است.",
ParameterName: "رطوبت",
Value: telemetry.HumidityPercent,
Status: "بالاتر"
));
}
else if (telemetry.HumidityPercent < settings.MinHumidityPercent)
{
alerts.Add(new AlertInfo(
Type: "Humidity",
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداقل مجاز ({settings.MinHumidityPercent}%) کمتر است.",
ParameterName: "رطوبت",
Value: telemetry.HumidityPercent,
Status: "پایین‌تر"
));
}
}
private void CheckSoilAlert(TelemetryDto telemetry, string deviceName, List<AlertInfo> alerts)
{
if (telemetry.SoilPercent > 100)
{
alerts.Add(new AlertInfo(
Type: "Soil",
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
ParameterName: "رطوبت خاک",
Value: telemetry.SoilPercent,
Status: "بالاتر"
));
}
else if (telemetry.SoilPercent < 0)
{
alerts.Add(new AlertInfo(
Type: "Soil",
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
ParameterName: "رطوبت خاک",
Value: telemetry.SoilPercent,
Status: "پایین‌تر"
));
}
}
private void CheckGasAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
{
if (telemetry.GasPPM > settings.MaxGasPPM)
{
alerts.Add(new AlertInfo(
Type: "Gas",
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداکثر مجاز ({settings.MaxGasPPM}) بیشتر است.",
ParameterName: "گاز Co",
Value: telemetry.GasPPM,
Status: "بالاتر"
));
}
else if (telemetry.GasPPM < settings.MinGasPPM)
{
alerts.Add(new AlertInfo(
Type: "Gas",
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداقل مجاز ({settings.MinGasPPM}) کمتر است.",
ParameterName: "گاز Co",
Value: telemetry.GasPPM,
Status: "پایین‌تر"
));
}
}
private void CheckLuxAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
{
if (telemetry.Lux > settings.MaxLux)
{
alerts.Add(new AlertInfo(
Type: "Lux",
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداکثر مجاز ({settings.MaxLux}) بیشتر است.",
ParameterName: "نور",
Value: telemetry.Lux,
Status: "بالاتر"
));
}
else if (telemetry.Lux < settings.MinLux)
{
alerts.Add(new AlertInfo(
Type: "Lux",
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداقل مجاز ({settings.MinLux}) کمتر است.",
ParameterName: "نور",
Value: telemetry.Lux,
Status: "پایین‌تر"
));
}
}
private async Task SendAlertIfNeededAsync(
int deviceId,
int userId,
string deviceName,
AlertInfo alert,
private async Task SendAlertForConditionAsync(
Domain.AlertCondition condition,
Domain.Device device,
TelemetryDto telemetry,
CancellationToken cancellationToken)
{
// Check if alert was sent in the last 10 minutes
var cooldownTime = DateTime.UtcNow.AddMinutes(-AlertCooldownMinutes);
// Determine cooldown based on notification type
var cooldownMinutes = condition.NotificationType == Domain.AlertNotificationType.Call
? condition.CallCooldownMinutes
: condition.SmsCooldownMinutes;
// Check if alert was sent recently
var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes);
var recentAlert = await dbContext.AlertNotifications
.Where(a => a.DeviceId == deviceId &&
a.UserId == userId &&
a.AlertType == alert.Type &&
.Where(a => a.DeviceId == device.Id &&
a.UserId == device.User.Id &&
a.AlertConditionId == condition.Id &&
a.SentAt >= cooldownTime)
.FirstOrDefaultAsync(cancellationToken);
if (recentAlert != null)
{
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, AlertType={AlertType}", deviceId, alert.Type);
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
device.Id, condition.Id, condition.NotificationType);
return;
}
// Get user to send SMS
var user = await dbContext.Users
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
// Build alert message
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
if (user == null || string.IsNullOrWhiteSpace(user.Mobile))
{
logger.LogWarning("User not found or mobile is empty: UserId={UserId}", userId);
return;
}
// Send SMS and collect response/errors
string? messageOutboxIdsJson = null;
// Send notification
string? messageOutboxIds = null;
string? errorMessage = null;
bool isSent = false;
try
{
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
{
Recipients = [user.Mobile],
PatternCode = "64di3w9kb0fxvif",
Variables = new Dictionary<string, string> {
{ "name", deviceName },
{ "parameter", alert.ParameterName },
{ "value", alert.Value.ToString("F1") },
{ "status", alert.Status },
}
}, cancellationToken);
if (smsResponse != null)
{
// Check if SMS was sent successfully
if (smsResponse.Meta.Status && smsResponse.Data != null && smsResponse.Data.MessageOutboxIds != null && smsResponse.Data.MessageOutboxIds.Count > 0)
{
// Success - save message outbox IDs
messageOutboxIdsJson = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
isSent = true;
logger.LogInformation("Alert SMS sent: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, OutboxIds={OutboxIds}",
deviceId, userId, alert.Type, messageOutboxIdsJson);
}
else
{
// Failed - save error from meta
var errors = new List<string>();
if (!string.IsNullOrWhiteSpace(smsResponse.Meta.Message))
{
errors.Add(smsResponse.Meta.Message);
}
if (smsResponse.Meta.Errors != null && smsResponse.Meta.Errors.Count > 0)
{
foreach (var error in smsResponse.Meta.Errors)
{
errors.Add($"{error.Key}: {string.Join(", ", error.Value)}");
}
}
if (errors.Count == 0)
{
errors.Add("SMS sending failed with unknown error");
}
errorMessage = string.Join(" | ", errors);
isSent = false;
logger.LogWarning("Alert SMS failed: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, Error={Error}",
deviceId, userId, alert.Type, errorMessage);
}
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
}
else
else // Call
{
errorMessage = "SMS service returned null response";
isSent = false;
logger.LogWarning("Alert SMS returned null: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}",
deviceId, userId, alert.Type);
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
}
}
catch (Exception ex)
@@ -296,17 +170,19 @@ public sealed class AlertService : IAlertService
errorMessage += $" | InnerException: {ex.InnerException.Message}";
}
isSent = false;
logger.LogError(ex, "Failed to send alert SMS: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}", deviceId, userId, alert.Type);
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
device.Id, condition.Id, condition.NotificationType);
}
// Save notification to database
var notification = new Domain.AlertNotification
{
DeviceId = deviceId,
UserId = userId,
AlertType = alert.Type,
Message = alert.Message,
MessageOutboxIds = messageOutboxIdsJson,
DeviceId = device.Id,
UserId = device.User.Id,
AlertConditionId = condition.Id,
NotificationType = condition.NotificationType,
Message = message,
MessageOutboxIds = messageOutboxIds,
ErrorMessage = errorMessage,
SentAt = DateTime.UtcNow,
IsSent = isSent
@@ -315,5 +191,137 @@ public sealed class AlertService : IAlertService
dbContext.AlertNotifications.Add(notification);
await dbContext.SaveChangesAsync(cancellationToken);
}
private string BuildAlertMessage(Domain.AlertCondition condition, string deviceName, TelemetryDto telemetry)
{
var parts = new List<string>();
parts.Add($"هشدار گلخانه {deviceName}:");
foreach (var rule in condition.Rules.OrderBy(r => r.Order))
{
var sensorName = rule.SensorType switch
{
Domain.SensorType.Temperature => "دما",
Domain.SensorType.Humidity => "رطوبت",
Domain.SensorType.Soil => "رطوبت خاک",
Domain.SensorType.Gas => "گاز",
Domain.SensorType.Lux => "نور",
_ => "سنسور"
};
var sensorValue = rule.SensorType switch
{
Domain.SensorType.Temperature => telemetry.TemperatureC,
Domain.SensorType.Humidity => telemetry.HumidityPercent,
Domain.SensorType.Soil => telemetry.SoilPercent,
Domain.SensorType.Gas => telemetry.GasPPM,
Domain.SensorType.Lux => telemetry.Lux,
_ => 0m
};
var unit = rule.SensorType switch
{
Domain.SensorType.Temperature => "°C",
Domain.SensorType.Humidity => "%",
Domain.SensorType.Soil => "%",
Domain.SensorType.Gas => "PPM",
Domain.SensorType.Lux => "لوکس",
_ => ""
};
var conditionText = rule.ComparisonType switch
{
Domain.ComparisonType.GreaterThan => $"{sensorName} ({sensorValue:F1}{unit}) بیشتر از {rule.Value1}{unit}",
Domain.ComparisonType.LessThan => $"{sensorName} ({sensorValue:F1}{unit}) کمتر از {rule.Value1}{unit}",
Domain.ComparisonType.Between => $"{sensorName} ({sensorValue:F1}{unit}) بین {rule.Value1} و {rule.Value2}{unit}",
Domain.ComparisonType.OutOfRange => $"{sensorName} ({sensorValue:F1}{unit}) خارج از محدوده {rule.Value1} تا {rule.Value2}{unit}",
_ => $"{sensorName}: {sensorValue:F1}{unit}"
};
parts.Add(conditionText);
}
return string.Join(" و ", parts);
}
private async Task<(bool isSent, string? messageOutboxIds, string? errorMessage)> SendSmsAlertAsync(
string mobile,
string deviceName,
string message,
CancellationToken cancellationToken)
{
try
{
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
{
Recipients = [mobile],
PatternCode = "64di3w9kb0fxvif",
Variables = new Dictionary<string, string> {
{ "name", deviceName },
{ "parameter", "شرایط" },
{ "value", message },
{ "status", "هشدار" }
}
}, cancellationToken);
if (smsResponse != null && smsResponse.Meta.Status &&
smsResponse.Data?.MessageOutboxIds != null &&
smsResponse.Data.MessageOutboxIds.Count > 0)
{
var outboxIds = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
logger.LogInformation("Alert SMS sent: Mobile={Mobile}, OutboxIds={OutboxIds}", mobile, outboxIds);
return (true, outboxIds, null);
}
else
{
var errors = new List<string>();
if (!string.IsNullOrWhiteSpace(smsResponse?.Meta.Message))
{
errors.Add(smsResponse.Meta.Message);
}
if (smsResponse?.Meta.Errors != null)
{
foreach (var error in smsResponse.Meta.Errors)
{
errors.Add($"{error.Key}: {string.Join(", ", error.Value)}");
}
}
var errorMsg = errors.Count > 0 ? string.Join(" | ", errors) : "Unknown SMS error";
logger.LogWarning("Alert SMS failed: Mobile={Mobile}, Error={Error}", mobile, errorMsg);
return (false, null, errorMsg);
}
}
catch (Exception ex)
{
var errorMsg = $"Exception: {ex.Message}";
logger.LogError(ex, "Exception sending SMS alert: Mobile={Mobile}", mobile);
return (false, null, errorMsg);
}
}
private async Task<(bool isSent, string? callId, string? errorMessage)> SendCallAlertAsync(
string mobile,
string deviceName,
string message,
CancellationToken cancellationToken)
{
try
{
// TODO: Implement voice call integration
// For now, just log and return success
logger.LogInformation("Voice call alert requested: Mobile={Mobile}, Message={Message}", mobile, message);
// Placeholder: In real implementation, call voiceCallService here
// var callResponse = await voiceCallService.MakeCallAsync(mobile, message, cancellationToken);
return (true, null, "Voice call not yet implemented");
}
catch (Exception ex)
{
var errorMsg = $"Exception: {ex.Message}";
logger.LogError(ex, "Exception sending call alert: Mobile={Mobile}", mobile);
return (false, null, errorMsg);
}
}
}

View 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;
}
}

View File

@@ -23,6 +23,8 @@
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,10 +9,14 @@ public sealed class GreenHomeDbContext : DbContext
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
public DbSet<Domain.AlertCondition> AlertConditions => Set<Domain.AlertCondition>();
public DbSet<Domain.AlertRule> AlertRules => Set<Domain.AlertRule>();
public DbSet<Domain.User> Users => Set<Domain.User>();
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -50,14 +54,10 @@ public sealed class GreenHomeDbContext : DbContext
{
b.ToTable("DeviceSettings");
b.HasKey(x => x.Id);
b.Property(x => x.DangerMaxTemperature).HasColumnType("decimal(18,2)");
b.Property(x => x.DangerMinTemperature).HasColumnType("decimal(18,2)");
b.Property(x => x.MaxTemperature).HasColumnType("decimal(18,2)");
b.Property(x => x.MinTemperature).HasColumnType("decimal(18,2)");
b.Property(x => x.MaxLux).HasColumnType("decimal(18,2)");
b.Property(x => x.MinLux).HasColumnType("decimal(18,2)");
b.Property(x => x.MaxHumidityPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.MinHumidityPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.Province).HasMaxLength(100);
b.Property(x => x.City).HasMaxLength(100);
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
@@ -65,6 +65,38 @@ public sealed class GreenHomeDbContext : DbContext
b.HasIndex(x => x.DeviceId).IsUnique();
});
modelBuilder.Entity<Domain.AlertCondition>(b =>
{
b.ToTable("AlertConditions");
b.HasKey(x => x.Id);
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
b.Property(x => x.TimeType).IsRequired().HasConversion<int>();
b.Property(x => x.CallCooldownMinutes).IsRequired();
b.Property(x => x.SmsCooldownMinutes).IsRequired();
b.Property(x => x.IsEnabled).IsRequired();
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DeviceId);
});
modelBuilder.Entity<Domain.AlertRule>(b =>
{
b.ToTable("AlertRules");
b.HasKey(x => x.Id);
b.Property(x => x.SensorType).IsRequired().HasConversion<int>();
b.Property(x => x.ComparisonType).IsRequired().HasConversion<int>();
b.Property(x => x.Value1).IsRequired().HasColumnType("decimal(18,2)");
b.Property(x => x.Value2).HasColumnType("decimal(18,2)");
b.Property(x => x.Order).IsRequired();
b.HasOne(x => x.AlertCondition)
.WithMany(c => c.Rules)
.HasForeignKey(x => x.AlertConditionId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.AlertConditionId);
});
modelBuilder.Entity<Domain.User>(b =>
{
b.ToTable("Users");
@@ -103,7 +135,7 @@ public sealed class GreenHomeDbContext : DbContext
{
b.ToTable("AlertNotifications");
b.HasKey(x => x.Id);
b.Property(x => x.AlertType).IsRequired().HasMaxLength(50);
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
b.Property(x => x.Message).IsRequired().HasMaxLength(500);
b.Property(x => x.MessageOutboxIds).HasMaxLength(500);
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
@@ -115,7 +147,47 @@ public sealed class GreenHomeDbContext : DbContext
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertType, x.SentAt });
b.HasOne(x => x.AlertCondition)
.WithMany()
.HasForeignKey(x => x.AlertConditionId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertConditionId, x.SentAt });
});
modelBuilder.Entity<Domain.AIQuery>(b =>
{
b.ToTable("AIQueries");
b.HasKey(x => x.Id);
b.Property(x => x.Question).IsRequired();
b.Property(x => x.Answer).IsRequired();
b.Property(x => x.Model).HasMaxLength(100);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.SetNull);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.SetNull);
b.HasIndex(x => x.DeviceId);
b.HasIndex(x => x.UserId);
b.HasIndex(x => x.CreatedAt);
});
modelBuilder.Entity<Domain.DailyReport>(b =>
{
b.ToTable("DailyReports");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Analysis).IsRequired();
b.Property(x => x.Model).HasMaxLength(100);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.DeviceId, x.PersianDate }).IsUnique();
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.CreatedAt);
});
}
}

View File

@@ -0,0 +1,453 @@
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216113127_AddAIQueryTable")]
partial class AddAIQueryTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int?>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<double?>("Temperature")
.HasColumnType("float");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId");
b.HasIndex("UserId");
b.ToTable("AIQueries", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AlertType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("ErrorMessage")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsSent")
.HasColumnType("bit");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MessageOutboxIds")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
b.ToTable("AlertNotifications", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<string>("Location")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Devices", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<decimal>("DangerMaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("DangerMinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("MaxGasPPM")
.HasColumnType("int");
b.Property<decimal>("MaxHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MaxLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("MinGasPPM")
.HasColumnType("int");
b.Property<decimal>("MinHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceSettings", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("DeviceId", "UserId");
b.HasIndex("UserId");
b.ToTable("DeviceUsers", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("GasPPM")
.HasColumnType("int");
b.Property<decimal>("HumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Lux")
.HasColumnType("decimal(18,2)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<DateTime>("ServerTimestampUtc")
.HasColumnType("datetime2");
b.Property<decimal>("SoilPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("TemperatureC")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("TimestampUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId", "ServerTimestampUtc");
b.HasIndex("DeviceId", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Family")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime2");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Role")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Mobile")
.IsUnique();
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<bool>("IsUsed")
.HasColumnType("bit");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<DateTime?>("UsedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("Mobile", "Code", "IsUsed");
b.ToTable("VerificationCodes", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany("DeviceUsers")
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany("DeviceUsers")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Navigation("DeviceUsers");
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Navigation("DeviceUsers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddAIQueryTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AIQueries",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: true),
Question = table.Column<string>(type: "nvarchar(max)", nullable: false),
Answer = table.Column<string>(type: "nvarchar(max)", nullable: false),
PromptTokens = table.Column<int>(type: "int", nullable: false),
CompletionTokens = table.Column<int>(type: "int", nullable: false),
TotalTokens = table.Column<int>(type: "int", nullable: false),
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Temperature = table.Column<double>(type: "float", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true),
UserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AIQueries", x => x.Id);
table.ForeignKey(
name: "FK_AIQueries_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_AIQueries_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_AIQueries_CreatedAt",
table: "AIQueries",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_AIQueries_DeviceId",
table: "AIQueries",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_AIQueries_UserId",
table: "AIQueries",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AIQueries");
}
}
}

View File

@@ -0,0 +1,530 @@
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216120357_adddailyreport")]
partial class adddailyreport
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int?>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<double?>("Temperature")
.HasColumnType("float");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId");
b.HasIndex("UserId");
b.ToTable("AIQueries", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AlertType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("ErrorMessage")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsSent")
.HasColumnType("bit");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MessageOutboxIds")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
b.ToTable("AlertNotifications", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Analysis")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianDay")
.HasColumnType("int");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<int>("RecordCount")
.HasColumnType("int");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<int>("SampledRecordCount")
.HasColumnType("int");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId", "PersianDate")
.IsUnique();
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("DailyReports", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<string>("Location")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Devices", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<decimal>("DangerMaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("DangerMinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("MaxGasPPM")
.HasColumnType("int");
b.Property<decimal>("MaxHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MaxLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("MinGasPPM")
.HasColumnType("int");
b.Property<decimal>("MinHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceSettings", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("DeviceId", "UserId");
b.HasIndex("UserId");
b.ToTable("DeviceUsers", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("GasPPM")
.HasColumnType("int");
b.Property<decimal>("HumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Lux")
.HasColumnType("decimal(18,2)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<DateTime>("ServerTimestampUtc")
.HasColumnType("datetime2");
b.Property<decimal>("SoilPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("TemperatureC")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("TimestampUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId", "ServerTimestampUtc");
b.HasIndex("DeviceId", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Family")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime2");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Role")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Mobile")
.IsUnique();
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<bool>("IsUsed")
.HasColumnType("bit");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<DateTime?>("UsedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("Mobile", "Code", "IsUsed");
b.ToTable("VerificationCodes", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany("DeviceUsers")
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany("DeviceUsers")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Navigation("DeviceUsers");
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Navigation("DeviceUsers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class adddailyreport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DailyReports",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
PersianYear = table.Column<int>(type: "int", nullable: false),
PersianMonth = table.Column<int>(type: "int", nullable: false),
PersianDay = table.Column<int>(type: "int", nullable: false),
Analysis = table.Column<string>(type: "nvarchar(max)", nullable: false),
RecordCount = table.Column<int>(type: "int", nullable: false),
SampledRecordCount = table.Column<int>(type: "int", nullable: false),
PromptTokens = table.Column<int>(type: "int", nullable: false),
CompletionTokens = table.Column<int>(type: "int", nullable: false),
TotalTokens = table.Column<int>(type: "int", nullable: false),
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DailyReports", x => x.Id);
table.ForeignKey(
name: "FK_DailyReports_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DailyReports_CreatedAt",
table: "DailyReports",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_DailyReports_DeviceId_PersianDate",
table: "DailyReports",
columns: new[] { "DeviceId", "PersianDate" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DailyReports_DeviceId_PersianYear_PersianMonth",
table: "DailyReports",
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DailyReports");
}
}
}

View File

@@ -0,0 +1,626 @@
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216131032_UpdateAlertSystemWithConditions")]
partial class UpdateAlertSystemWithConditions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int?>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<double?>("Temperature")
.HasColumnType("float");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId");
b.HasIndex("UserId");
b.ToTable("AIQueries", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CallCooldownMinutes")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<int>("NotificationType")
.HasColumnType("int");
b.Property<int>("SmsCooldownMinutes")
.HasColumnType("int");
b.Property<int>("TimeType")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.ToTable("AlertConditions", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("ErrorMessage")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsSent")
.HasColumnType("bit");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MessageOutboxIds")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("NotificationType")
.HasColumnType("int");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime2");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AlertConditionId");
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt");
b.ToTable("AlertNotifications", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("ComparisonType")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("SensorType")
.HasColumnType("int");
b.Property<decimal>("Value1")
.HasColumnType("decimal(18,2)");
b.Property<decimal?>("Value2")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("AlertConditionId");
b.ToTable("AlertRules", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Analysis")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianDay")
.HasColumnType("int");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<int>("RecordCount")
.HasColumnType("int");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<int>("SampledRecordCount")
.HasColumnType("int");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId", "PersianDate")
.IsUnique();
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("DailyReports", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<string>("Location")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Devices", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("City")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<decimal?>("Latitude")
.HasColumnType("decimal(9,6)");
b.Property<decimal?>("Longitude")
.HasColumnType("decimal(9,6)");
b.Property<string>("Province")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceSettings", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("DeviceId", "UserId");
b.HasIndex("UserId");
b.ToTable("DeviceUsers", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("GasPPM")
.HasColumnType("int");
b.Property<decimal>("HumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Lux")
.HasColumnType("decimal(18,2)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<DateTime>("ServerTimestampUtc")
.HasColumnType("datetime2");
b.Property<decimal>("SoilPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("TemperatureC")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("TimestampUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId", "ServerTimestampUtc");
b.HasIndex("DeviceId", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Family")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime2");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Role")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Mobile")
.IsUnique();
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(4)
.HasColumnType("nvarchar(4)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<bool>("IsUsed")
.HasColumnType("bit");
b.Property<string>("Mobile")
.IsRequired()
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<DateTime?>("UsedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("Mobile", "Code", "IsUsed");
b.ToTable("VerificationCodes", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany()
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AlertCondition");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany("Rules")
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AlertCondition");
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany("DeviceUsers")
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GreenHome.Domain.User", "User")
.WithMany("DeviceUsers")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.Navigation("Rules");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Navigation("DeviceUsers");
});
modelBuilder.Entity("GreenHome.Domain.User", b =>
{
b.Navigation("DeviceUsers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,312 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class UpdateAlertSystemWithConditions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_AlertNotifications_DeviceId_UserId_AlertType_SentAt",
table: "AlertNotifications");
migrationBuilder.DropColumn(
name: "DangerMaxTemperature",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "DangerMinTemperature",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MaxGasPPM",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MaxHumidityPercent",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MaxLux",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MaxTemperature",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MinGasPPM",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MinHumidityPercent",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MinLux",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "MinTemperature",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "AlertType",
table: "AlertNotifications");
migrationBuilder.AddColumn<string>(
name: "City",
table: "DeviceSettings",
type: "nvarchar(100)",
maxLength: 100,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<decimal>(
name: "Latitude",
table: "DeviceSettings",
type: "decimal(9,6)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "Longitude",
table: "DeviceSettings",
type: "decimal(9,6)",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Province",
table: "DeviceSettings",
type: "nvarchar(100)",
maxLength: 100,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "AlertConditionId",
table: "AlertNotifications",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "NotificationType",
table: "AlertNotifications",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "AlertConditions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
NotificationType = table.Column<int>(type: "int", nullable: false),
TimeType = table.Column<int>(type: "int", nullable: false),
CallCooldownMinutes = table.Column<int>(type: "int", nullable: false),
SmsCooldownMinutes = table.Column<int>(type: "int", nullable: false),
IsEnabled = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AlertConditions", x => x.Id);
table.ForeignKey(
name: "FK_AlertConditions_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AlertRules",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AlertConditionId = table.Column<int>(type: "int", nullable: false),
SensorType = table.Column<int>(type: "int", nullable: false),
ComparisonType = table.Column<int>(type: "int", nullable: false),
Value1 = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
Value2 = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
Order = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AlertRules", x => x.Id);
table.ForeignKey(
name: "FK_AlertRules_AlertConditions_AlertConditionId",
column: x => x.AlertConditionId,
principalTable: "AlertConditions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AlertNotifications_AlertConditionId",
table: "AlertNotifications",
column: "AlertConditionId");
migrationBuilder.CreateIndex(
name: "IX_AlertNotifications_DeviceId_UserId_AlertConditionId_SentAt",
table: "AlertNotifications",
columns: new[] { "DeviceId", "UserId", "AlertConditionId", "SentAt" });
migrationBuilder.CreateIndex(
name: "IX_AlertConditions_DeviceId",
table: "AlertConditions",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_AlertRules_AlertConditionId",
table: "AlertRules",
column: "AlertConditionId");
migrationBuilder.AddForeignKey(
name: "FK_AlertNotifications_AlertConditions_AlertConditionId",
table: "AlertNotifications",
column: "AlertConditionId",
principalTable: "AlertConditions",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AlertNotifications_AlertConditions_AlertConditionId",
table: "AlertNotifications");
migrationBuilder.DropTable(
name: "AlertRules");
migrationBuilder.DropTable(
name: "AlertConditions");
migrationBuilder.DropIndex(
name: "IX_AlertNotifications_AlertConditionId",
table: "AlertNotifications");
migrationBuilder.DropIndex(
name: "IX_AlertNotifications_DeviceId_UserId_AlertConditionId_SentAt",
table: "AlertNotifications");
migrationBuilder.DropColumn(
name: "City",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "Latitude",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "Longitude",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "Province",
table: "DeviceSettings");
migrationBuilder.DropColumn(
name: "AlertConditionId",
table: "AlertNotifications");
migrationBuilder.DropColumn(
name: "NotificationType",
table: "AlertNotifications");
migrationBuilder.AddColumn<decimal>(
name: "DangerMaxTemperature",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "DangerMinTemperature",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<int>(
name: "MaxGasPPM",
table: "DeviceSettings",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<decimal>(
name: "MaxHumidityPercent",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "MaxLux",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "MaxTemperature",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<int>(
name: "MinGasPPM",
table: "DeviceSettings",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<decimal>(
name: "MinHumidityPercent",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "MinLux",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "MinTemperature",
table: "DeviceSettings",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "AlertType",
table: "AlertNotifications",
type: "nvarchar(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.CreateIndex(
name: "IX_AlertNotifications_DeviceId_UserId_AlertType_SentAt",
table: "AlertNotifications",
columns: new[] { "DeviceId", "UserId", "AlertType", "SentAt" });
}
}
}

View File

@@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddDailyReportsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DailyReports",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
PersianYear = table.Column<int>(type: "int", nullable: false),
PersianMonth = table.Column<int>(type: "int", nullable: false),
PersianDay = table.Column<int>(type: "int", nullable: false),
Analysis = table.Column<string>(type: "nvarchar(max)", nullable: false),
RecordCount = table.Column<int>(type: "int", nullable: false),
SampledRecordCount = table.Column<int>(type: "int", nullable: false),
PromptTokens = table.Column<int>(type: "int", nullable: false),
CompletionTokens = table.Column<int>(type: "int", nullable: false),
TotalTokens = table.Column<int>(type: "int", nullable: false),
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DailyReports", x => x.Id);
table.ForeignKey(
name: "FK_DailyReports_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DailyReports_CreatedAt",
table: "DailyReports",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_DailyReports_DeviceId_PersianDate",
table: "DailyReports",
columns: new[] { "DeviceId", "PersianDate" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DailyReports_DeviceId_PersianYear_PersianMonth",
table: "DailyReports",
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DailyReports");
}
}
}

View File

@@ -22,6 +22,100 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Answer")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int?>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<string>("Question")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<double?>("Temperature")
.HasColumnType("float");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId");
b.HasIndex("UserId");
b.ToTable("AIQueries", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("CallCooldownMinutes")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<int>("NotificationType")
.HasColumnType("int");
b.Property<int>("SmsCooldownMinutes")
.HasColumnType("int");
b.Property<int>("TimeType")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.ToTable("AlertConditions", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.Property<int>("Id")
@@ -30,10 +124,8 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AlertType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("DeviceId")
.HasColumnType("int");
@@ -54,6 +146,9 @@ namespace GreenHome.Infrastructure.Migrations
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("NotificationType")
.HasColumnType("int");
b.Property<DateTime>("SentAt")
.HasColumnType("datetime2");
@@ -62,13 +157,114 @@ namespace GreenHome.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("AlertConditionId");
b.HasIndex("UserId");
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt");
b.ToTable("AlertNotifications", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AlertConditionId")
.HasColumnType("int");
b.Property<int>("ComparisonType")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("SensorType")
.HasColumnType("int");
b.Property<decimal>("Value1")
.HasColumnType("decimal(18,2)");
b.Property<decimal?>("Value2")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("AlertConditionId");
b.ToTable("AlertRules", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Analysis")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("CompletionTokens")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<string>("Model")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianDay")
.HasColumnType("int");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<int>("PromptTokens")
.HasColumnType("int");
b.Property<int>("RecordCount")
.HasColumnType("int");
b.Property<long?>("ResponseTimeMs")
.HasColumnType("bigint");
b.Property<int>("SampledRecordCount")
.HasColumnType("int");
b.Property<int>("TotalTokens")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DeviceId", "PersianDate")
.IsUnique();
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("DailyReports", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Property<int>("Id")
@@ -110,41 +306,27 @@ namespace GreenHome.Infrastructure.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("City")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<decimal>("DangerMaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("DangerMinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("MaxGasPPM")
.HasColumnType("int");
b.Property<decimal?>("Latitude")
.HasColumnType("decimal(9,6)");
b.Property<decimal>("MaxHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal?>("Longitude")
.HasColumnType("decimal(9,6)");
b.Property<decimal>("MaxLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MaxTemperature")
.HasColumnType("decimal(18,2)");
b.Property<int>("MinGasPPM")
.HasColumnType("int");
b.Property<decimal>("MinHumidityPercent")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinLux")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("MinTemperature")
.HasColumnType("decimal(18,2)");
b.Property<string>("Province")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
@@ -303,8 +485,42 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("VerificationCodes", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("GreenHome.Domain.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany()
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
@@ -317,11 +533,35 @@ namespace GreenHome.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AlertCondition");
b.Navigation("Device");
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
{
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
.WithMany("Rules")
.HasForeignKey("AlertConditionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AlertCondition");
});
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.HasOne("GreenHome.Domain.User", "User")
@@ -363,6 +603,11 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User");
});
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
{
b.Navigation("Rules");
});
modelBuilder.Entity("GreenHome.Domain.Device", b =>
{
b.Navigation("DeviceUsers");

View File

@@ -0,0 +1,125 @@
using GreenHome.Application;
namespace GreenHome.Infrastructure;
/// <summary>
/// سرویس محاسبه طلوع و غروب خورشید
/// </summary>
public sealed class SunCalculatorService : ISunCalculatorService
{
public bool IsDaytime(DateTime dateTime, decimal latitude, decimal longitude)
{
var lat = (double)latitude;
var lng = (double)longitude;
// Calculate sunrise and sunset times
var (sunrise, sunset) = CalculateSunriseSunset(dateTime, lat, lng);
// Check if current time is between sunrise and sunset
var currentTime = dateTime.TimeOfDay;
return currentTime >= sunrise && currentTime <= sunset;
}
private (TimeSpan sunrise, TimeSpan sunset) CalculateSunriseSunset(DateTime date, double latitude, double longitude)
{
// Julian day calculation
var julianDay = CalculateJulianDay(date);
var julianCentury = (julianDay - 2451545.0) / 36525.0;
// Sun's mean longitude
var sunMeanLongitude = (280.46646 + julianCentury * (36000.76983 + julianCentury * 0.0003032)) % 360;
// Sun's mean anomaly
var sunMeanAnomaly = 357.52911 + julianCentury * (35999.05029 - 0.0001537 * julianCentury);
// Earth's orbit eccentricity
var eccentricity = 0.016708634 - julianCentury * (0.000042037 + 0.0000001267 * julianCentury);
// Sun's equation of center
var sunCenter = Math.Sin(ToRadians(sunMeanAnomaly)) * (1.914602 - julianCentury * (0.004817 + 0.000014 * julianCentury))
+ Math.Sin(ToRadians(2 * sunMeanAnomaly)) * (0.019993 - 0.000101 * julianCentury)
+ Math.Sin(ToRadians(3 * sunMeanAnomaly)) * 0.000289;
// Sun's true longitude
var sunTrueLongitude = sunMeanLongitude + sunCenter;
// Sun's apparent longitude
var sunApparentLongitude = sunTrueLongitude - 0.00569 - 0.00478 * Math.Sin(ToRadians(125.04 - 1934.136 * julianCentury));
// Mean oblique ecliptic
var meanOblique = 23.0 + (26.0 + ((21.448 - julianCentury * (46.815 + julianCentury * (0.00059 - julianCentury * 0.001813)))) / 60.0) / 60.0;
// Oblique correction
var obliqueCorrection = meanOblique + 0.00256 * Math.Cos(ToRadians(125.04 - 1934.136 * julianCentury));
// Sun's declination
var declination = ToDegrees(Math.Asin(Math.Sin(ToRadians(obliqueCorrection)) * Math.Sin(ToRadians(sunApparentLongitude))));
// Equation of time
var y = Math.Tan(ToRadians(obliqueCorrection / 2.0)) * Math.Tan(ToRadians(obliqueCorrection / 2.0));
var equationOfTime = 4.0 * ToDegrees(y * Math.Sin(2.0 * ToRadians(sunMeanLongitude))
- 2.0 * eccentricity * Math.Sin(ToRadians(sunMeanAnomaly))
+ 4.0 * eccentricity * y * Math.Sin(ToRadians(sunMeanAnomaly)) * Math.Cos(2.0 * ToRadians(sunMeanLongitude))
- 0.5 * y * y * Math.Sin(4.0 * ToRadians(sunMeanLongitude))
- 1.25 * eccentricity * eccentricity * Math.Sin(2.0 * ToRadians(sunMeanAnomaly)));
// Hour angle sunrise (civil twilight: sun 6 degrees below horizon)
var zenith = 90.833; // Official: 90 degrees 50 minutes
var hourAngle = ToDegrees(Math.Acos(
(Math.Cos(ToRadians(zenith)) / (Math.Cos(ToRadians(latitude)) * Math.Cos(ToRadians(declination))))
- Math.Tan(ToRadians(latitude)) * Math.Tan(ToRadians(declination))
));
// Calculate sunrise and sunset in minutes
var solarNoon = (720.0 - 4.0 * longitude - equationOfTime) / 1440.0;
var sunriseTime = solarNoon - hourAngle * 4.0 / 1440.0;
var sunsetTime = solarNoon + hourAngle * 4.0 / 1440.0;
// Convert to local time (assume UTC offset for Iran: +3:30 = 210 minutes)
// You should ideally calculate timezone offset based on longitude
var utcOffsetMinutes = Math.Round(longitude / 15.0) * 60.0;
var sunriseMinutes = sunriseTime * 1440.0 + utcOffsetMinutes;
var sunsetMinutes = sunsetTime * 1440.0 + utcOffsetMinutes;
// Handle edge cases
if (sunriseMinutes < 0) sunriseMinutes += 1440;
if (sunriseMinutes >= 1440) sunriseMinutes -= 1440;
if (sunsetMinutes < 0) sunsetMinutes += 1440;
if (sunsetMinutes >= 1440) sunsetMinutes -= 1440;
var sunrise = TimeSpan.FromMinutes(sunriseMinutes);
var sunset = TimeSpan.FromMinutes(sunsetMinutes);
return (sunrise, sunset);
}
private double CalculateJulianDay(DateTime date)
{
var year = date.Year;
var month = date.Month;
var day = date.Day;
if (month <= 2)
{
year -= 1;
month += 12;
}
var a = year / 100;
var b = 2 - a + (a / 4);
return Math.Floor(365.25 * (year + 4716)) + Math.Floor(30.6001 * (month + 1)) + day + b - 1524.5;
}
private double ToRadians(double degrees)
{
return degrees * Math.PI / 180.0;
}
private double ToDegrees(double radians)
{
return radians * 180.0 / Math.PI;
}
}