refactor: reorganize Infrastructure layer into folder structure with DI

This commit is contained in:
2026-06-01 18:15:58 +03:30
parent 775ae0d9b4
commit 9fe575ae07
55 changed files with 2960 additions and 3031 deletions

View File

@@ -1,376 +1,394 @@
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
public sealed class GreenHomeDbContext : DbContext
{
public GreenHomeDbContext(DbContextOptions<GreenHomeDbContext> options) : base(options) { }
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.AlertLog> AlertLogs => Set<Domain.AlertLog>();
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Domain.Device>(b =>
{
b.ToTable("Devices");
b.HasKey(x => x.Id);
b.Property(x => x.DeviceName).IsRequired().HasMaxLength(10);
b.Property(x => x.UserId).IsRequired();
b.Property(x => x.Location).HasMaxLength(250);
b.Property(x => x.NeshanLocation).HasMaxLength(80);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Domain.TelemetryRecord>(b =>
{
b.ToTable("Telemetry");
b.HasKey(x => x.Id);
b.Property(x => x.TemperatureC).HasColumnType("decimal(18,2)");
b.Property(x => x.HumidityPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.SoilPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.Lux).HasColumnType("decimal(18,2)");
b.Property(x => x.PersianDate).HasMaxLength(10);
b.Property(x => x.Voltage).HasColumnType("decimal(18,2)");
b.Property(x => x.BatteryVoltage).HasColumnType("decimal(18,2)");
b.Property(x => x.BatteryPercent).HasColumnType("decimal(18,2)");
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => new { x.DeviceId, x.TimestampUtc });
b.HasIndex(x => new { x.DeviceId, x.ServerTimestampUtc });
});
modelBuilder.Entity<Domain.DeviceSettings>(b =>
{
b.ToTable("DeviceSettings");
b.HasKey(x => x.Id);
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.Property(x => x.ProductType).HasMaxLength(100);
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
b.Property(x => x.SmsRecievers).HasMaxLength(120);
b.Property(x => x.RelayCount).HasDefaultValue(4);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
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");
b.HasKey(x => x.Id);
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
b.Property(x => x.Name).IsRequired().HasMaxLength(100);
b.Property(x => x.Family).IsRequired().HasMaxLength(100);
b.Property(x => x.Role).IsRequired().HasConversion<int>();
b.HasIndex(x => x.Mobile).IsUnique();
});
modelBuilder.Entity<Domain.DeviceUser>(b =>
{
b.ToTable("DeviceUsers");
b.HasKey(x => new { x.DeviceId, x.UserId });
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
b.HasOne(x => x.Device)
.WithMany(d => d.DeviceUsers)
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.User)
.WithMany(u => u.DeviceUsers)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Domain.VerificationCode>(b =>
{
b.ToTable("VerificationCodes");
b.HasKey(x => x.Id);
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
b.Property(x => x.Code).IsRequired().HasMaxLength(4);
b.HasIndex(x => new { x.Mobile, x.Code, x.IsUsed });
});
modelBuilder.Entity<Domain.AlertNotification>(b =>
{
b.ToTable("AlertNotifications");
b.HasKey(x => x.Id);
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);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
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);
});
modelBuilder.Entity<Domain.AlertLog>(b =>
{
b.ToTable("AlertLogs");
b.HasKey(x => x.Id);
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
b.Property(x => x.Status).IsRequired().HasConversion<int>();
b.Property(x => x.ErrorMessage).HasMaxLength(2000);
b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.AlertCondition)
.WithMany()
.HasForeignKey(x => x.AlertConditionId)
.OnDelete(DeleteBehavior.SetNull);
b.HasIndex(x => new { x.DeviceId, x.SentAt });
b.HasIndex(x => new { x.UserId, x.SentAt });
b.HasIndex(x => x.AlertType);
b.HasIndex(x => x.Status);
});
modelBuilder.Entity<Domain.UserDailyReport>(b =>
{
b.ToTable("UserDailyReports");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Observations).IsRequired();
b.Property(x => x.Operations).IsRequired();
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.PersianDate });
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.UserId);
});
modelBuilder.Entity<Domain.ReportImage>(b =>
{
b.ToTable("ReportImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.Property(x => x.Description).HasMaxLength(500);
b.HasOne(x => x.UserDailyReport)
.WithMany(r => r.Images)
.HasForeignKey(x => x.UserDailyReportId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.UserDailyReportId);
});
modelBuilder.Entity<Domain.Checklist>(b =>
{
b.ToTable("Checklists");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.IsActive).IsRequired();
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CreatedByUser)
.WithMany()
.HasForeignKey(x => x.CreatedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.IsActive });
});
modelBuilder.Entity<Domain.ChecklistItem>(b =>
{
b.ToTable("ChecklistItems");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(300);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.Order).IsRequired();
b.Property(x => x.IsRequired).IsRequired();
b.HasOne(x => x.Checklist)
.WithMany(c => c.Items)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.ChecklistId, x.Order });
});
modelBuilder.Entity<Domain.ChecklistCompletion>(b =>
{
b.ToTable("ChecklistCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Checklist)
.WithMany(c => c.Completions)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CompletedByUser)
.WithMany()
.HasForeignKey(x => x.CompletedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.ChecklistId, x.PersianDate });
b.HasIndex(x => x.CompletedByUserId);
});
modelBuilder.Entity<Domain.ChecklistItemCompletion>(b =>
{
b.ToTable("ChecklistItemCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.IsChecked).IsRequired();
b.Property(x => x.Note).HasMaxLength(500);
b.HasOne(x => x.ChecklistCompletion)
.WithMany(cc => cc.ItemCompletions)
.HasForeignKey(x => x.ChecklistCompletionId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.ChecklistItem)
.WithMany()
.HasForeignKey(x => x.ChecklistItemId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => x.ChecklistCompletionId);
});
modelBuilder.Entity<Domain.DevicePost>(b =>
{
b.ToTable("DevicePosts");
b.HasKey(x => x.Id);
b.Property(x => x.Content).IsRequired().HasMaxLength(5000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.AuthorUser)
.WithMany()
.HasForeignKey(x => x.AuthorUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.CreatedAt });
b.HasIndex(x => x.AuthorUserId);
});
modelBuilder.Entity<Domain.DevicePostImage>(b =>
{
b.ToTable("DevicePostImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.HasOne(x => x.DevicePost)
.WithMany(p => p.Images)
.HasForeignKey(x => x.DevicePostId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DevicePostId);
});
}
}
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure.Data;
public sealed class GreenHomeDbContext : DbContext
{
public GreenHomeDbContext(DbContextOptions<GreenHomeDbContext> options) : base(options) { }
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.AlertLog> AlertLogs => Set<Domain.AlertLog>();
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
public DbSet<Domain.PowerOutageLog> PowerOutageLogs => Set<Domain.PowerOutageLog>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Domain.Device>(b =>
{
b.ToTable("Devices");
b.HasKey(x => x.Id);
b.Property(x => x.DeviceName).IsRequired().HasMaxLength(10);
b.Property(x => x.UserId).IsRequired();
b.Property(x => x.Location).HasMaxLength(250);
b.Property(x => x.NeshanLocation).HasMaxLength(80);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Domain.TelemetryRecord>(b =>
{
b.ToTable("Telemetry");
b.HasKey(x => x.Id);
b.Property(x => x.TemperatureC).HasColumnType("decimal(18,2)");
b.Property(x => x.HumidityPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.SoilPercent).HasColumnType("decimal(18,2)");
b.Property(x => x.Lux).HasColumnType("decimal(18,2)");
b.Property(x => x.PersianDate).HasMaxLength(10);
b.Property(x => x.Voltage).HasColumnType("decimal(18,2)");
b.Property(x => x.BatteryVoltage).HasColumnType("decimal(18,2)");
b.Property(x => x.BatteryPercent).HasColumnType("decimal(18,2)");
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => new { x.DeviceId, x.TimestampUtc });
b.HasIndex(x => new { x.DeviceId, x.ServerTimestampUtc });
});
modelBuilder.Entity<Domain.DeviceSettings>(b =>
{
b.ToTable("DeviceSettings");
b.HasKey(x => x.Id);
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.Property(x => x.ProductType).HasMaxLength(100);
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
b.Property(x => x.SmsRecievers).HasMaxLength(120);
b.Property(x => x.RelayCount).HasDefaultValue(4);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
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");
b.HasKey(x => x.Id);
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
b.Property(x => x.Name).IsRequired().HasMaxLength(100);
b.Property(x => x.Family).IsRequired().HasMaxLength(100);
b.Property(x => x.Role).IsRequired().HasConversion<int>();
b.HasIndex(x => x.Mobile).IsUnique();
});
modelBuilder.Entity<Domain.DeviceUser>(b =>
{
b.ToTable("DeviceUsers");
b.HasKey(x => new { x.DeviceId, x.UserId });
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
b.HasOne(x => x.Device)
.WithMany(d => d.DeviceUsers)
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.User)
.WithMany(u => u.DeviceUsers)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Domain.VerificationCode>(b =>
{
b.ToTable("VerificationCodes");
b.HasKey(x => x.Id);
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
b.Property(x => x.Code).IsRequired().HasMaxLength(4);
b.HasIndex(x => new { x.Mobile, x.Code, x.IsUsed });
});
modelBuilder.Entity<Domain.AlertNotification>(b =>
{
b.ToTable("AlertNotifications");
b.HasKey(x => x.Id);
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);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
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);
});
modelBuilder.Entity<Domain.AlertLog>(b =>
{
b.ToTable("AlertLogs");
b.HasKey(x => x.Id);
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
b.Property(x => x.Status).IsRequired().HasConversion<int>();
b.Property(x => x.ErrorMessage).HasMaxLength(2000);
b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.AlertCondition)
.WithMany()
.HasForeignKey(x => x.AlertConditionId)
.OnDelete(DeleteBehavior.SetNull);
b.HasIndex(x => new { x.DeviceId, x.SentAt });
b.HasIndex(x => new { x.UserId, x.SentAt });
b.HasIndex(x => x.AlertType);
b.HasIndex(x => x.Status);
});
modelBuilder.Entity<Domain.UserDailyReport>(b =>
{
b.ToTable("UserDailyReports");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Observations).IsRequired();
b.Property(x => x.Operations).IsRequired();
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.PersianDate });
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.UserId);
});
modelBuilder.Entity<Domain.ReportImage>(b =>
{
b.ToTable("ReportImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.Property(x => x.Description).HasMaxLength(500);
b.HasOne(x => x.UserDailyReport)
.WithMany(r => r.Images)
.HasForeignKey(x => x.UserDailyReportId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.UserDailyReportId);
});
modelBuilder.Entity<Domain.Checklist>(b =>
{
b.ToTable("Checklists");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.IsActive).IsRequired();
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CreatedByUser)
.WithMany()
.HasForeignKey(x => x.CreatedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.IsActive });
});
modelBuilder.Entity<Domain.ChecklistItem>(b =>
{
b.ToTable("ChecklistItems");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired().HasMaxLength(300);
b.Property(x => x.Description).HasMaxLength(1000);
b.Property(x => x.Order).IsRequired();
b.Property(x => x.IsRequired).IsRequired();
b.HasOne(x => x.Checklist)
.WithMany(c => c.Items)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.ChecklistId, x.Order });
});
modelBuilder.Entity<Domain.ChecklistCompletion>(b =>
{
b.ToTable("ChecklistCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
b.Property(x => x.Notes).HasMaxLength(2000);
b.HasOne(x => x.Checklist)
.WithMany(c => c.Completions)
.HasForeignKey(x => x.ChecklistId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.CompletedByUser)
.WithMany()
.HasForeignKey(x => x.CompletedByUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.ChecklistId, x.PersianDate });
b.HasIndex(x => x.CompletedByUserId);
});
modelBuilder.Entity<Domain.ChecklistItemCompletion>(b =>
{
b.ToTable("ChecklistItemCompletions");
b.HasKey(x => x.Id);
b.Property(x => x.IsChecked).IsRequired();
b.Property(x => x.Note).HasMaxLength(500);
b.HasOne(x => x.ChecklistCompletion)
.WithMany(cc => cc.ItemCompletions)
.HasForeignKey(x => x.ChecklistCompletionId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.ChecklistItem)
.WithMany()
.HasForeignKey(x => x.ChecklistItemId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => x.ChecklistCompletionId);
});
modelBuilder.Entity<Domain.DevicePost>(b =>
{
b.ToTable("DevicePosts");
b.HasKey(x => x.Id);
b.Property(x => x.Content).IsRequired().HasMaxLength(5000);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.AuthorUser)
.WithMany()
.HasForeignKey(x => x.AuthorUserId)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.DeviceId, x.CreatedAt });
b.HasIndex(x => x.AuthorUserId);
});
modelBuilder.Entity<Domain.DevicePostImage>(b =>
{
b.ToTable("DevicePostImages");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
b.HasOne(x => x.DevicePost)
.WithMany(p => p.Images)
.HasForeignKey(x => x.DevicePostId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DevicePostId);
});
modelBuilder.Entity<Domain.PowerOutageLog>(b =>
{
b.ToTable("PowerOutageLogs");
b.HasKey(x => x.Id);
b.Property(x => x.PreviousPower).IsRequired();
b.Property(x => x.CurrentPower).IsRequired();
b.Property(x => x.EventType).IsRequired().HasConversion<int>();
b.Property(x => x.PersianDate).HasMaxLength(10);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.DeviceId, x.TimestampUtc });
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.EventType);
});
}
}

View File

@@ -1,114 +1,114 @@
// <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("20251005195032_init")]
partial class init
{
/// <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.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>("Mobile")
.HasColumnType("nvarchar(max)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<string>("Owner")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Devices", (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<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", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
#pragma warning restore 612, 618
}
}
}
// <auto-generated />
using System;
using GreenHome.Infrastructure.Data;
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.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251005195032_init")]
partial class init
{
/// <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.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>("Mobile")
.HasColumnType("nvarchar(max)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<string>("Owner")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Devices", (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<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", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,74 +1,74 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Devices",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceName = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Owner = table.Column<string>(type: "nvarchar(max)", nullable: false),
Mobile = table.Column<string>(type: "nvarchar(max)", nullable: true),
Location = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
NeshanLocation = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Devices", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Telemetry",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
TimestampUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
TemperatureC = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
HumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
SoilPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
GasPPM = table.Column<int>(type: "int", nullable: false),
Lux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
PersianYear = table.Column<int>(type: "int", nullable: false),
PersianMonth = table.Column<int>(type: "int", nullable: false),
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Telemetry", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Telemetry_DeviceId_PersianYear_PersianMonth",
table: "Telemetry",
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
migrationBuilder.CreateIndex(
name: "IX_Telemetry_DeviceId_TimestampUtc",
table: "Telemetry",
columns: new[] { "DeviceId", "TimestampUtc" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Devices");
migrationBuilder.DropTable(
name: "Telemetry");
}
}
}
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Devices",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceName = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Owner = table.Column<string>(type: "nvarchar(max)", nullable: false),
Mobile = table.Column<string>(type: "nvarchar(max)", nullable: true),
Location = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
NeshanLocation = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Devices", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Telemetry",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
TimestampUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
TemperatureC = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
HumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
SoilPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
GasPPM = table.Column<int>(type: "int", nullable: false),
Lux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
PersianYear = table.Column<int>(type: "int", nullable: false),
PersianMonth = table.Column<int>(type: "int", nullable: false),
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Telemetry", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Telemetry_DeviceId_PersianYear_PersianMonth",
table: "Telemetry",
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
migrationBuilder.CreateIndex(
name: "IX_Telemetry_DeviceId_TimestampUtc",
table: "Telemetry",
columns: new[] { "DeviceId", "TimestampUtc" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Devices");
migrationBuilder.DropTable(
name: "Telemetry");
}
}
}

View File

@@ -1,180 +1,180 @@
// <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("20251008205106_addminmaxdanger")]
partial class addminmaxdanger
{
/// <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.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>("Mobile")
.HasColumnType("nvarchar(max)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<string>("Owner")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
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.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<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", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
#pragma warning restore 612, 618
}
}
}
// <auto-generated />
using System;
using GreenHome.Infrastructure.Data;
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.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251008205106_addminmaxdanger")]
partial class addminmaxdanger
{
/// <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.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>("Mobile")
.HasColumnType("nvarchar(max)");
b.Property<string>("NeshanLocation")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("nvarchar(80)");
b.Property<string>("Owner")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
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.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<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", "TimestampUtc");
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
b.ToTable("Telemetry", (string)null);
});
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
{
b.HasOne("GreenHome.Domain.Device", "Device")
.WithMany()
.HasForeignKey("DeviceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Device");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,59 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class addminmaxdanger : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DeviceSettings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
DangerMaxTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
DangerMinTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxGasPPM = table.Column<int>(type: "int", nullable: false),
MinGasPPM = table.Column<int>(type: "int", nullable: false),
MaxLux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinLux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxHumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinHumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceSettings", x => x.Id);
table.ForeignKey(
name: "FK_DeviceSettings_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DeviceSettings_DeviceId",
table: "DeviceSettings",
column: "DeviceId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeviceSettings");
}
}
}
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class addminmaxdanger : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DeviceSettings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
DangerMaxTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
DangerMinTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinTemperature = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxGasPPM = table.Column<int>(type: "int", nullable: false),
MinGasPPM = table.Column<int>(type: "int", nullable: false),
MaxLux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinLux = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MaxHumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
MinHumidityPercent = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceSettings", x => x.Id);
table.ForeignKey(
name: "FK_DeviceSettings_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DeviceSettings_DeviceId",
table: "DeviceSettings",
column: "DeviceId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeviceSettings");
}
}
}

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251118204845_UpdateDeviceUserRelationship")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class UpdateDeviceUserRelationship : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251119133827_AddUserRolesAndDeviceUsers")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddUserRolesAndDeviceUsers : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251119135914_AddAlertNotifications")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddAlertNotifications : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251119142729_FixAlertNotifications")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class FixAlertNotifications : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251127125625_AddServerTimestampUtc")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddServerTimestampUtc : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216113127_AddAIQueryTable")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddAIQueryTable : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216120357_adddailyreport")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class adddailyreport : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216131032_UpdateAlertSystemWithConditions")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class UpdateAlertSystemWithConditions : Migration

View File

@@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddDailyReportsTable : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251216204600_AddAllNewFeatures")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddAllNewFeatures : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20251217154130_AddDeviceTokenAndPhoneFields")]

View File

@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddDeviceTokenAndPhoneFields : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20260106150615_POWER_VOLTAGE_FIELDS")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class POWER_VOLTAGE_FIELDS : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20260106151206_FIX_VOLTAGE_TYPE")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class FIX_VOLTAGE_TYPE : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20260107144244_AddSmsRecievers")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddSmsRecievers : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20260113032844_AddBatteryInfoFields")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddBatteryInfoFields : Migration

View File

@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
[DbContext(typeof(GreenHomeDbContext))]
[Migration("20260428054608_AddRelayCountToDeviceSettings")]

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Migrations
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddRelayCountToDeviceSettings : Migration

View File

@@ -0,0 +1,70 @@
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Infrastructure.Services.AI;
using GreenHome.Infrastructure.Services.Alert;
using GreenHome.Infrastructure.Services.Auth;
using GreenHome.Infrastructure.Services.Checklist;
using GreenHome.Infrastructure.Services.Device;
using GreenHome.Infrastructure.Services.PowerOutage;
using GreenHome.Infrastructure.Services.Report;
using GreenHome.Infrastructure.Services.Sun;
using GreenHome.Infrastructure.Services.Telemetry;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace GreenHome.Infrastructure;
/// <summary>
/// Registers all Infrastructure-layer services with the DI container.
/// Call this from Program.cs to keep startup clean.
/// </summary>
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Entity Framework
services.AddDbContext<GreenHomeDbContext>(options =>
{
options.UseSqlServer(configuration.GetConnectionString("Default"));
});
// Auth
services.AddScoped<IAuthService, AuthService>();
// Alert
services.AddScoped<IAlertService, AlertService>();
services.AddScoped<IAlertConditionService, AlertConditionService>();
services.AddScoped<IAlertLogService, AlertLogService>();
// Device
services.AddScoped<IDeviceService, DeviceService>();
services.AddScoped<IDeviceSettingsService, DeviceSettingsService>();
services.AddScoped<IDeviceTokenService, DeviceTokenService>();
services.AddScoped<IDevicePostService, DevicePostService>();
// Telemetry
services.AddScoped<ITelemetryService, TelemetryService>();
// Reports
services.AddScoped<IDailyReportService, DailyReportService>();
services.AddScoped<IMonthlyReportService, MonthlyReportService>();
services.AddScoped<IUserDailyReportService, UserDailyReportService>();
// Checklist
services.AddScoped<IChecklistService, ChecklistService>();
// Power Outage
services.AddScoped<IPowerOutageLogService, PowerOutageLogService>();
// AI
services.AddScoped<IAIQueryService, AIQueryService>();
// Sun Calculator
services.AddScoped<ISunCalculatorService, SunCalculatorService>();
return services;
}
}

View File

@@ -0,0 +1,51 @@
using System.Globalization;
namespace GreenHome.Infrastructure.Extensions;
/// <summary>
/// Helper methods for Persian (Solar Hijri) date calculations.
/// Eliminates duplicate PersianCalendar code across services.
/// </summary>
public static class PersianDateHelper
{
private static readonly PersianCalendar PersianCalendar = new();
/// <summary>
/// Extracts Persian year, month, day from a DateTime and returns the formatted Persian date string (yyyy/MM/dd).
/// </summary>
public static (int Year, int Month, int Day, string PersianDate) ToPersianDate(this DateTime dateTime)
{
var localDt = dateTime.Kind == DateTimeKind.Utc ? dateTime.ToLocalTime() : dateTime;
var year = PersianCalendar.GetYear(localDt);
var month = PersianCalendar.GetMonth(localDt);
var day = PersianCalendar.GetDayOfMonth(localDt);
var persianDate = $"{year:0000}/{month:00}/{day:00}";
return (year, month, day, persianDate);
}
/// <summary>
/// Validates a Persian date string in yyyy/MM/dd format and extracts year, month, day.
/// </summary>
public static bool TryParsePersianDate(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

@@ -1,48 +1,49 @@
using GreenHome.Application;
using GreenHome.Domain;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.AI;
public sealed class AIQueryService : IAIQueryService
{
private readonly GreenHomeDbContext dbContext;
private readonly ILogger<AIQueryService> logger;
private readonly GreenHomeDbContext _dbContext;
private readonly ILogger<AIQueryService> _logger;
public AIQueryService(
GreenHomeDbContext dbContext,
ILogger<AIQueryService> logger)
{
this.dbContext = dbContext;
this.logger = logger;
_dbContext = dbContext;
_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}",
_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");
_logger.LogError(ex, "Error saving AI query");
throw;
}
}
public async Task<List<AIQuery>> GetDeviceQueriesAsync(
int deviceId,
int take = 50,
int deviceId,
int take = 50,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
return await _dbContext.AIQueries
.Where(q => q.DeviceId == deviceId)
.OrderByDescending(q => q.CreatedAt)
.Take(take)
@@ -50,11 +51,11 @@ public sealed class AIQueryService : IAIQueryService
}
public async Task<List<AIQuery>> GetUserQueriesAsync(
int userId,
int take = 50,
int userId,
int take = 50,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
return await _dbContext.AIQueries
.Where(q => q.UserId == userId)
.OrderByDescending(q => q.CreatedAt)
.Take(take)
@@ -62,28 +63,28 @@ public sealed class AIQueryService : IAIQueryService
}
public async Task<int> GetDeviceTotalTokensAsync(
int deviceId,
int deviceId,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
return await _dbContext.AIQueries
.Where(q => q.DeviceId == deviceId)
.SumAsync(q => q.TotalTokens, cancellationToken);
}
public async Task<int> GetUserTotalTokensAsync(
int userId,
int userId,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
return await _dbContext.AIQueries
.Where(q => q.UserId == userId)
.SumAsync(q => q.TotalTokens, cancellationToken);
}
public async Task<List<AIQuery>> GetRecentQueriesAsync(
int take = 20,
int take = 20,
CancellationToken cancellationToken = default)
{
return await dbContext.AIQueries
return await _dbContext.AIQueries
.Include(q => q.Device)
.Include(q => q.User)
.OrderByDescending(q => q.CreatedAt)
@@ -94,15 +95,15 @@ public sealed class AIQueryService : IAIQueryService
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
var allQueries = await _dbContext.AIQueries
.Select(q => new
{
q.TotalTokens,
q.PromptTokens,
q.CompletionTokens,
q.ResponseTimeMs,
q.CreatedAt
})
.ToListAsync(cancellationToken);
@@ -114,13 +115,12 @@ public sealed class AIQueryService : IAIQueryService
TotalTokensUsed = allQueries.Sum(q => q.TotalTokens),
TotalPromptTokens = allQueries.Sum(q => q.PromptTokens),
TotalCompletionTokens = allQueries.Sum(q => q.CompletionTokens),
AverageResponseTimeMs = allQueries.Any()
AverageResponseTimeMs = allQueries.Any()
? allQueries.Where(q => q.ResponseTimeMs.HasValue)
.Average(q => q.ResponseTimeMs ?? 0)
.Average(q => q.ResponseTimeMs ?? 0)
: 0,
TodayQueries = todayQueries.Count,
TodayTokens = todayQueries.Sum(q => q.TotalTokens)
};
}
}

View File

@@ -1,23 +1,24 @@
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Alert;
public sealed class AlertConditionService : IAlertConditionService
{
private readonly GreenHomeDbContext dbContext;
private readonly IMapper mapper;
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public AlertConditionService(GreenHomeDbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<IReadOnlyList<AlertConditionDto>> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
{
var conditions = await dbContext.AlertConditions
var conditions = await _dbContext.AlertConditions
.Include(x => x.Device)
.Include(x => x.Rules)
.Where(x => x.DeviceId == deviceId)
@@ -25,18 +26,18 @@ public sealed class AlertConditionService : IAlertConditionService
.AsNoTracking()
.ToListAsync(cancellationToken);
return mapper.Map<List<AlertConditionDto>>(conditions);
return _mapper.Map<List<AlertConditionDto>>(conditions);
}
public async Task<AlertConditionDto?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
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;
return condition != null ? _mapper.Map<AlertConditionDto>(condition) : null;
}
public async Task<int> CreateAsync(CreateAlertConditionRequest request, CancellationToken cancellationToken)
@@ -53,29 +54,26 @@ public sealed class AlertConditionService : IAlertConditionService
UpdatedAt = DateTime.UtcNow
};
// Add rules
var rules = mapper.Map<List<Domain.AlertRule>>(request.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);
_dbContext.AlertConditions.Add(condition);
await _dbContext.SaveChangesAsync(cancellationToken);
return condition.Id;
}
public async Task UpdateAsync(UpdateAlertConditionRequest request, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
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;
@@ -84,53 +82,48 @@ public sealed class AlertConditionService : IAlertConditionService
condition.IsEnabled = request.IsEnabled;
condition.UpdatedAt = DateTime.UtcNow;
// Remove old rules and add new ones
dbContext.AlertRules.RemoveRange(condition.Rules);
_dbContext.AlertRules.RemoveRange(condition.Rules);
condition.Rules.Clear();
var newRules = mapper.Map<List<Domain.AlertRule>>(request.Rules);
var newRules = _mapper.Map<List<Domain.AlertRule>>(request.Rules);
foreach (var rule in newRules)
{
condition.Rules.Add(rule);
}
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(int id, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
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);
_dbContext.AlertConditions.Remove(condition);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<bool> ToggleEnabledAsync(int id, bool isEnabled, CancellationToken cancellationToken)
{
var condition = await dbContext.AlertConditions
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);
await _dbContext.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<IReadOnlyList<RelayConditionDto>> GetRelayConditionsAsync(int deviceId, CancellationToken cancellationToken)
{
var conditions = await dbContext.AlertConditions
var conditions = await _dbContext.AlertConditions
.Include(x => x.Rules)
.Where(x => x.DeviceId == deviceId && (int)x.NotificationType > 0)
.OrderByDescending(x => x.CreatedAt)
@@ -156,31 +149,23 @@ public sealed class AlertConditionService : IAlertConditionService
public async Task<IReadOnlyList<RelayConditionDto>> GetUpdatedRelayConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken)
{
// اگر lastUpdateDate null باشه، یعنی کلاینت تا حالا هیچ داده‌ای نگرفته، همه رو برگردون
if (lastUpdateDate == null)
{
return await GetRelayConditionsAsync(deviceId, cancellationToken);
}
var hasUpdates = await dbContext.AlertConditions
var hasUpdates = await _dbContext.AlertConditions
.AnyAsync(x => x.DeviceId == deviceId && (int)x.NotificationType > 0 && x.UpdatedAt > lastUpdateDate, cancellationToken);
if (!hasUpdates)
{
return Array.Empty<RelayConditionDto>();
}
return await GetRelayConditionsAsync(deviceId, cancellationToken);
}
public async Task<bool> HasUpdatedConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken)
{
if(lastUpdateDate == null) return true;
if (lastUpdateDate == null) return true;
var hasUpdates = await dbContext.AlertConditions
return await _dbContext.AlertConditions
.AnyAsync(x => x.DeviceId == deviceId && (int)x.NotificationType > 0 && x.UpdatedAt > lastUpdateDate, cancellationToken);
return hasUpdates;
}
}

View File

@@ -1,65 +1,51 @@
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Alert;
public sealed class AlertLogService : IAlertLogService
{
private readonly GreenHomeDbContext _context;
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public AlertLogService(GreenHomeDbContext context, IMapper mapper)
{
_context = context;
_dbContext = context;
_mapper = mapper;
}
public async Task<PagedResult<AlertLogDto>> GetAlertLogsAsync(
AlertLogFilter filter,
AlertLogFilter filter,
CancellationToken cancellationToken)
{
var query = _context.AlertLogs
var query = _dbContext.AlertLogs
.Include(x => x.Device)
.Include(x => x.User)
.AsNoTracking()
.AsQueryable();
// Apply filters
if (filter.DeviceId.HasValue)
{
query = query.Where(x => x.DeviceId == filter.DeviceId.Value);
}
if (filter.UserId.HasValue)
{
query = query.Where(x => x.UserId == filter.UserId.Value);
}
if (filter.AlertType.HasValue)
{
query = query.Where(x => x.AlertType == filter.AlertType.Value);
}
if (filter.Status.HasValue)
{
query = query.Where(x => x.Status == filter.Status.Value);
}
if (filter.StartDate.HasValue)
{
query = query.Where(x => x.SentAt >= filter.StartDate.Value);
}
if (filter.EndDate.HasValue)
{
query = query.Where(x => x.SentAt <= filter.EndDate.Value);
}
// Get total count
var totalCount = await query.CountAsync(cancellationToken);
// Apply pagination and ordering
var items = await query
.OrderByDescending(x => x.SentAt)
.Skip((filter.Page - 1) * filter.PageSize)
@@ -79,7 +65,7 @@ public sealed class AlertLogService : IAlertLogService
public async Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken)
{
var log = await _context.AlertLogs
var log = await _dbContext.AlertLogs
.Include(x => x.Device)
.Include(x => x.User)
.Include(x => x.AlertCondition)
@@ -92,9 +78,8 @@ public sealed class AlertLogService : IAlertLogService
public async Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken)
{
var entity = _mapper.Map<Domain.AlertLog>(dto);
_context.AlertLogs.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.AlertLogs.Add(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
}

View File

@@ -1,24 +1,23 @@
using GreenHome.Application;
using GreenHome.Domain;
using GreenHome.Infrastructure.Data;
using GreenHome.Sms.Ippanel;
using GreenHome.VoiceCall.Avanak;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text.Json;
using static GreenHome.Sms.Ippanel.IppanelSmsService;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Alert;
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 readonly GreenHomeDbContext _dbContext;
private readonly IDeviceSettingsService _deviceSettingsService;
private readonly ISmsService _smsService;
private readonly IVoiceCallService _voiceCallService;
private readonly ISunCalculatorService _sunCalculatorService;
private readonly IPowerOutageLogService _powerOutageLogService;
private readonly ILogger<AlertService> _logger;
public AlertService(
GreenHomeDbContext dbContext,
@@ -26,20 +25,21 @@ public sealed class AlertService : IAlertService
ISmsService smsService,
IVoiceCallService voiceCallService,
ISunCalculatorService sunCalculatorService,
IPowerOutageLogService powerOutageLogService,
ILogger<AlertService> logger)
{
this.dbContext = dbContext;
this.deviceSettingsService = deviceSettingsService;
this.smsService = smsService;
this.voiceCallService = voiceCallService;
this.sunCalculatorService = sunCalculatorService;
this.logger = logger;
_dbContext = dbContext;
_deviceSettingsService = deviceSettingsService;
_smsService = smsService;
_voiceCallService = voiceCallService;
_sunCalculatorService = sunCalculatorService;
_powerOutageLogService = powerOutageLogService;
_logger = logger;
}
public async Task<string?> CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
public async Task<AlertCheckResult?> CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
{
// Get device with all users who should receive alerts
var device = await dbContext.Devices
var device = await _dbContext.Devices
.Include(d => d.User)
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
.ThenInclude(du => du.User)
@@ -47,105 +47,104 @@ public sealed class AlertService : IAlertService
if (device == null)
{
logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
_logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
return null;
}
// Get device settings for location
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
var settings = await _deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
var recievers = device.User.Mobile;
recievers += "," + (settings?.SmsRecievers ?? "");
recievers = string.Join(",",
recievers
var recievers = string.Join(",",
(device.User.Mobile + "," + (settings?.SmsRecievers ?? ""))
.Split(',')
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.Distinct()
);
// Get all users who should receive alerts
// var usersToAlert = device.DeviceUsers
// .Where(du => du.ReceiveAlerts)
// .Select(du => du.User)
// .ToList();
if (recievers.Length == 0)
{
logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
_logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
return null;
}
// Get all enabled alert conditions for this device
var conditions = await dbContext.AlertConditions
var conditions = await _dbContext.AlertConditions
.Include(c => c.Rules)
.Where(c => c.DeviceId == deviceId && c.IsEnabled)
.ToListAsync(cancellationToken);
if (!conditions.Any())
{
logger.LogDebug("No enabled alert conditions for device: DeviceId={DeviceId}", deviceId);
//return null;
_logger.LogDebug("No enabled alert conditions for device: DeviceId={DeviceId}", deviceId);
}
// 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);
isDaytime = _sunCalculatorService.IsDaytime(DateTime.UtcNow, settings.Latitude.Value, settings.Longitude.Value);
}
List<string> messages = [];
// 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
}
continue;
if (condition.TimeType == Domain.AlertTimeType.Night && isDaytime == true)
{
continue; // This condition is for nighttime only, but it's daytime
}
continue;
// Check if all rules match (AND logic)
var allRulesMatch = condition.Rules.All(rule => CheckRule(rule, telemetry));
if (allRulesMatch && condition.Rules.Count != 0)
{
// All rules passed, send alert if cooldown period has passed
// await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken);
AlertNotification notif = await GetAlertMessage(condition, device, device.User, telemetry, cancellationToken);
if(notif != null && !string.IsNullOrEmpty(notif.Message))
if (notif != null && !string.IsNullOrEmpty(notif.Message))
{
messages.Add(notif.Message);
await dbContext.AlertNotifications.AddAsync(notif, cancellationToken);
await _dbContext.AlertNotifications.AddAsync(notif, cancellationToken);
}
// return message;
}
}
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
if(telemetry.OldPower == 0 && telemetry.Power == 1)
var result = new AlertCheckResult();
if (telemetry.OldPower == 0 && telemetry.Power == 1)
{
result.PowerStatus = "1";
messages.Add($"برق دستگاه {device.DeviceName} متصل شد");
await _powerOutageLogService.LogPowerChangeAsync(
deviceId,
telemetry.OldPower.Value,
telemetry.Power.Value,
telemetry.TimestampUtc ?? DateTime.UtcNow,
cancellationToken);
}
else if(telemetry.OldPower == 1 && telemetry.Power == 0)
else if (telemetry.OldPower == 1 && telemetry.Power == 0)
{
result.PowerStatus = "0";
messages.Add($"برق دستگاه {device.DeviceName} قطع شد");
await _powerOutageLogService.LogPowerChangeAsync(
deviceId,
telemetry.OldPower.Value,
telemetry.Power.Value,
telemetry.TimestampUtc ?? DateTime.UtcNow,
cancellationToken);
}
if(messages.Any())
if (messages.Any())
{
return $"tt{recievers}#{string.Join("@", messages)}";
result.Messages = messages;
result.Recipients = recievers;
return result;
}
if (result.PowerStatus != null)
return result;
return null;
}
private bool CheckRule(Domain.AlertRule rule, TelemetryDto telemetry)
{
// Get sensor value
var sensorValue = rule.SensorType switch
{
Domain.SensorType.Temperature => telemetry.TemperatureC,
@@ -156,7 +155,6 @@ public sealed class AlertService : IAlertService
_ => 0m
};
// Check comparison
return rule.ComparisonType switch
{
Domain.ComparisonType.GreaterThan => sensorValue > rule.Value1,
@@ -174,17 +172,15 @@ public sealed class AlertService : IAlertService
TelemetryDto telemetry,
CancellationToken cancellationToken)
{
// Determine cooldown based on notification type
var cooldownMinutes = condition.NotificationType == Domain.AlertNotificationType.Call
? condition.CallCooldownMinutes
: condition.SmsCooldownMinutes;
// Build alert message once
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
var sentAt = DateTime.UtcNow;
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
var recentAlert = await dbContext.AlertNotifications
var recentAlert = await _dbContext.AlertNotifications
.Where(a => a.DeviceId == device.Id &&
a.UserId == user.Id &&
a.AlertConditionId == condition.Id &&
@@ -193,7 +189,7 @@ public sealed class AlertService : IAlertService
if (recentAlert != null)
{
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
_logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
device.Id, user.Id, condition.Id);
return null;
}
@@ -205,14 +201,9 @@ public sealed class AlertService : IAlertService
AlertConditionId = condition.Id,
NotificationType = condition.NotificationType,
Message = message,
//MessageOutboxIds = messageOutboxIds,
//ErrorMessage = errorMessage,
SentAt = sentAt,
//IsSent = true
};
// dbContext.AlertNotifications.Add(notification);
return notification;
}
@@ -223,21 +214,17 @@ public sealed class AlertService : IAlertService
TelemetryDto telemetry,
CancellationToken cancellationToken)
{
// Determine cooldown based on notification type
var cooldownMinutes = condition.NotificationType == Domain.AlertNotificationType.Call
? condition.CallCooldownMinutes
: condition.SmsCooldownMinutes;
// Build alert message once
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
var sentAt = DateTime.UtcNow;
// Send alert to each user
foreach (var user in usersToAlert)
{
// Check if alert was sent recently to this user
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
var recentAlert = await dbContext.AlertNotifications
var recentAlert = await _dbContext.AlertNotifications
.Where(a => a.DeviceId == device.Id &&
a.UserId == user.Id &&
a.AlertConditionId == condition.Id &&
@@ -246,12 +233,11 @@ public sealed class AlertService : IAlertService
if (recentAlert != null)
{
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
_logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
device.Id, user.Id, condition.Id);
continue;
}
// Send notification
var startTime = DateTime.UtcNow;
string? messageOutboxIds = null;
string? errorMessage = null;
@@ -263,7 +249,7 @@ public sealed class AlertService : IAlertService
{
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
}
else // Call
else
{
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
}
@@ -272,17 +258,14 @@ public sealed class AlertService : IAlertService
{
errorMessage = $"Exception: {ex.Message}";
if (ex.InnerException != null)
{
errorMessage += $" | InnerException: {ex.InnerException.Message}";
}
isSent = false;
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
_logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
device.Id, user.Id, condition.Id);
}
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
// Save notification to database (old table for backwards compatibility)
var notification = new Domain.AlertNotification
{
DeviceId = device.Id,
@@ -296,9 +279,8 @@ public sealed class AlertService : IAlertService
IsSent = isSent
};
dbContext.AlertNotifications.Add(notification);
_dbContext.AlertNotifications.Add(notification);
// Log the alert
var alertLog = new Domain.AlertLog
{
DeviceId = device.Id,
@@ -314,10 +296,10 @@ public sealed class AlertService : IAlertService
ProcessingTimeMs = processingTime
};
dbContext.AlertLogs.Add(alertLog);
_dbContext.AlertLogs.Add(alertLog);
}
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
private string BuildAlertMessage(Domain.AlertCondition condition, string deviceName, TelemetryDto telemetry)
@@ -380,7 +362,7 @@ public sealed class AlertService : IAlertService
{
try
{
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
var smsResponse = await _smsService.SendPatternSmsAsync(new PatternSmsRequest
{
Recipients = [mobile],
PatternCode = "64di3w9kb0fxvif",
@@ -397,32 +379,28 @@ public sealed class AlertService : IAlertService
smsResponse.Data.MessageOutboxIds.Count > 0)
{
var outboxIds = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
logger.LogInformation("Alert SMS sent: Mobile={Mobile}, OutboxIds={OutboxIds}", mobile, outboxIds);
_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);
_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);
_logger.LogError(ex, "Exception sending SMS alert: Mobile={Mobile}", mobile);
return (false, null, errorMsg);
}
}
@@ -435,38 +413,30 @@ public sealed class AlertService : IAlertService
{
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);
_logger.LogInformation("Voice call alert requested: Mobile={Mobile}, Message={Message}", mobile, message);
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);
_logger.LogError(ex, "Exception sending call alert: Mobile={Mobile}", mobile);
return (false, null, errorMsg);
}
}
public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken)
{
// Get device with all users who should receive alerts
var device = await dbContext.Devices
var device = await _dbContext.Devices
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
.ThenInclude(du => du.User)
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
if (device == null)
{
logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
_logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
}
// Get all users who should receive alerts
var usersToAlert = device.DeviceUsers
.Where(du => du.ReceiveAlerts)
.Select(du => du.User)
@@ -474,32 +444,22 @@ public sealed class AlertService : IAlertService
if (usersToAlert.Count == 0)
{
logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
_logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
return;
}
var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است.";
var sentAt = DateTime.UtcNow;
// Send to all users (both SMS and Call for power outage - it's critical!)
foreach (var user in usersToAlert)
{
// Send SMS
await SendPowerOutageNotificationAsync(
device, user, message, sentAt,
Domain.AlertNotificationType.SMS,
cancellationToken);
// Send Call (important alert)
await SendPowerOutageNotificationAsync(
device, user, message, sentAt,
Domain.AlertNotificationType.Call,
cancellationToken);
await SendPowerOutageNotificationAsync(device, user, message, sentAt, Domain.AlertNotificationType.SMS, cancellationToken);
await SendPowerOutageNotificationAsync(device, user, message, sentAt, Domain.AlertNotificationType.Call, cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
usersToAlert.Count, deviceId);
}
@@ -519,31 +479,22 @@ public sealed class AlertService : IAlertService
try
{
if (notificationType == Domain.AlertNotificationType.SMS)
{
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(
user.Mobile, device.DeviceName, message, cancellationToken);
}
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
else
{
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(
user.Mobile, device.DeviceName, message, cancellationToken);
}
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
}
catch (Exception ex)
{
errorMessage = $"Exception: {ex.Message}";
if (ex.InnerException != null)
{
errorMessage += $" | InnerException: {ex.InnerException.Message}";
}
isSent = false;
logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
_logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
device.Id, user.Id, notificationType);
}
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
// Save notification (old table)
var notification = new Domain.AlertNotification
{
DeviceId = device.Id,
@@ -557,9 +508,8 @@ public sealed class AlertService : IAlertService
IsSent = isSent
};
dbContext.AlertNotifications.Add(notification);
_dbContext.AlertNotifications.Add(notification);
// Log the alert
var alertLog = new Domain.AlertLog
{
DeviceId = device.Id,
@@ -575,7 +525,6 @@ public sealed class AlertService : IAlertService
ProcessingTimeMs = processingTime
};
dbContext.AlertLogs.Add(alertLog);
_dbContext.AlertLogs.Add(alertLog);
}
}

View File

@@ -1,17 +1,18 @@
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Sms.Ippanel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
using System.Text;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Auth;
public sealed class AuthService : IAuthService
{
private readonly GreenHomeDbContext dbContext;
private readonly ISmsService smsService;
private readonly ILogger<AuthService> logger;
private readonly GreenHomeDbContext _dbContext;
private readonly ISmsService _smsService;
private readonly ILogger<AuthService> _logger;
private const int CodeExpirationMinutes = 5;
private const int ResendCooldownSeconds = 120;
@@ -20,16 +21,15 @@ public sealed class AuthService : IAuthService
ISmsService smsService,
ILogger<AuthService> logger)
{
this.dbContext = dbContext;
this.smsService = smsService;
this.logger = logger;
_dbContext = dbContext;
_smsService = smsService;
_logger = logger;
}
public async Task<SendCodeResponse> SendVerificationCodeAsync(
SendCodeRequest request,
CancellationToken cancellationToken)
{
// Normalize mobile number (remove spaces, dashes, etc.)
var mobile = NormalizeMobile(request.Mobile);
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length != 11 || !mobile.StartsWith("09"))
@@ -41,11 +41,10 @@ public sealed class AuthService : IAuthService
};
}
// Check if we can resend (cooldown period)
var canResend = await CanResendCodeAsync(mobile, cancellationToken);
if (!canResend)
{
var lastCode = await dbContext.VerificationCodes
var lastCode = await _dbContext.VerificationCodes
.Where(v => v.Mobile == mobile && !v.IsUsed)
.OrderByDescending(v => v.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
@@ -62,11 +61,9 @@ public sealed class AuthService : IAuthService
}
}
// Generate 4-digit code
var code = GenerateCode();
// Invalidate previous unused codes for this mobile
var previousCodes = await dbContext.VerificationCodes
var previousCodes = await _dbContext.VerificationCodes
.Where(v => v.Mobile == mobile && !v.IsUsed)
.ToListAsync(cancellationToken);
@@ -76,7 +73,6 @@ public sealed class AuthService : IAuthService
prevCode.UsedAt = DateTime.UtcNow;
}
// Create new verification code
var verificationCode = new Domain.VerificationCode
{
Mobile = mobile,
@@ -86,25 +82,23 @@ public sealed class AuthService : IAuthService
IsUsed = false
};
dbContext.VerificationCodes.Add(verificationCode);
await dbContext.SaveChangesAsync(cancellationToken);
_dbContext.VerificationCodes.Add(verificationCode);
await _dbContext.SaveChangesAsync(cancellationToken);
// Send SMS
try
{
await smsService.SendPatternSmsAsync(new PatternSmsRequest
await _smsService.SendPatternSmsAsync(new PatternSmsRequest
{
Recipients = [mobile],
PatternCode = "ruvpjx7lajne1dx",
Variables = new Dictionary<string, string> { { "code", code } }
}, cancellationToken);
logger.LogInformation("Verification code sent to {Mobile}", mobile);
_logger.LogInformation("Verification code sent to {Mobile}", mobile);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send SMS to {Mobile}", mobile);
// Continue even if SMS fails (for development/testing)
_logger.LogError(ex, "Failed to send SMS to {Mobile}", mobile);
}
return new SendCodeResponse
@@ -131,8 +125,7 @@ public sealed class AuthService : IAuthService
};
}
// Find valid verification code
var verificationCode = await dbContext.VerificationCodes
var verificationCode = await _dbContext.VerificationCodes
.Where(v => v.Mobile == mobile && v.Code == code && !v.IsUsed && v.ExpiresAt > DateTime.UtcNow)
.OrderByDescending(v => v.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
@@ -146,12 +139,10 @@ public sealed class AuthService : IAuthService
};
}
// Mark code as used
verificationCode.IsUsed = true;
verificationCode.UsedAt = DateTime.UtcNow;
// Find or create user
var user = await dbContext.Users
var user = await _dbContext.Users
.FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken);
if (user == null)
@@ -162,16 +153,15 @@ public sealed class AuthService : IAuthService
CreatedAt = DateTime.UtcNow,
LastLoginAt = DateTime.UtcNow
};
dbContext.Users.Add(user);
_dbContext.Users.Add(user);
}
else
{
user.LastLoginAt = DateTime.UtcNow;
}
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
// Generate simple token (in production, use JWT)
var token = GenerateToken(user.Id, mobile);
return new VerifyCodeResponse
@@ -193,7 +183,7 @@ public sealed class AuthService : IAuthService
public async Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken)
{
var normalizedMobile = NormalizeMobile(mobile);
var lastCode = await dbContext.VerificationCodes
var lastCode = await _dbContext.VerificationCodes
.Where(v => v.Mobile == normalizedMobile && !v.IsUsed)
.OrderByDescending(v => v.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
@@ -213,7 +203,6 @@ public sealed class AuthService : IAuthService
private static string GenerateToken(int userId, string mobile)
{
// Simple token generation (in production, use JWT)
var data = $"{userId}:{mobile}:{DateTime.UtcNow:yyyyMMddHHmmss}";
var bytes = Encoding.UTF8.GetBytes(data);
var hash = SHA256.HashData(bytes);
@@ -225,10 +214,8 @@ public sealed class AuthService : IAuthService
if (string.IsNullOrWhiteSpace(mobile))
return string.Empty;
// Remove all non-digit characters
var normalized = new string(mobile.Where(char.IsDigit).ToArray());
// Convert to standard format (09xxxxxxxxx)
if (normalized.StartsWith("9") && normalized.Length == 10)
normalized = "0" + normalized;
else if (normalized.StartsWith("0098") && normalized.Length == 13)
@@ -239,4 +226,3 @@ public sealed class AuthService : IAuthService
return normalized;
}
}

View File

@@ -1,8 +1,9 @@
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Checklist;
public sealed class ChecklistService : IChecklistService
{
@@ -149,4 +150,3 @@ public sealed class ChecklistService : IChecklistService
return completion.Id;
}
}

View File

@@ -1,17 +1,18 @@
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Device;
public sealed class DevicePostService : IDevicePostService
{
private readonly GreenHomeDbContext _context;
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public DevicePostService(GreenHomeDbContext context, IMapper mapper)
{
_context = context;
_dbContext = context;
_mapper = mapper;
}
@@ -19,16 +20,14 @@ public sealed class DevicePostService : IDevicePostService
DevicePostFilter filter,
CancellationToken cancellationToken)
{
var query = _context.DevicePosts
var query = _dbContext.DevicePosts
.Include(p => p.AuthorUser)
.Include(p => p.Images)
.AsNoTracking()
.Where(p => p.DeviceId == filter.DeviceId);
if (filter.AuthorUserId.HasValue)
{
query = query.Where(p => p.AuthorUserId == filter.AuthorUserId.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
@@ -51,7 +50,7 @@ public sealed class DevicePostService : IDevicePostService
public async Task<DevicePostDto?> GetPostByIdAsync(int id, CancellationToken cancellationToken)
{
var post = await _context.DevicePosts
var post = await _dbContext.DevicePosts
.Include(p => p.AuthorUser)
.Include(p => p.Images)
.AsNoTracking()
@@ -64,12 +63,9 @@ public sealed class DevicePostService : IDevicePostService
CreateDevicePostRequest request,
CancellationToken cancellationToken)
{
// Verify user has access to device
var hasAccess = await CanUserAccessDeviceAsync(request.AuthorUserId, request.DeviceId, cancellationToken);
if (!hasAccess)
{
throw new UnauthorizedAccessException("کاربر به این دستگاه دسترسی ندارد");
}
var post = new Domain.DevicePost
{
@@ -79,8 +75,8 @@ public sealed class DevicePostService : IDevicePostService
CreatedAt = DateTime.UtcNow
};
_context.DevicePosts.Add(post);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.DevicePosts.Add(post);
await _dbContext.SaveChangesAsync(cancellationToken);
return post.Id;
}
@@ -89,33 +85,29 @@ public sealed class DevicePostService : IDevicePostService
UpdateDevicePostRequest request,
CancellationToken cancellationToken)
{
var post = await _context.DevicePosts
var post = await _dbContext.DevicePosts
.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken);
if (post == null)
{
throw new InvalidOperationException($"پست با شناسه {request.Id} یافت نشد");
}
post.Content = request.Content;
post.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeletePostAsync(int id, CancellationToken cancellationToken)
{
var post = await _context.DevicePosts
var post = await _dbContext.DevicePosts
.Include(p => p.Images)
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
if (post == null)
{
throw new InvalidOperationException($"پست با شناسه {id} یافت نشد");
}
_context.DevicePosts.Remove(post);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.DevicePosts.Remove(post);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<int> AddImageToPostAsync(
@@ -126,13 +118,11 @@ public sealed class DevicePostService : IDevicePostService
long fileSize,
CancellationToken cancellationToken)
{
var post = await _context.DevicePosts
var post = await _dbContext.DevicePosts
.FirstOrDefaultAsync(p => p.Id == postId, cancellationToken);
if (post == null)
{
throw new InvalidOperationException($"پست با شناسه {postId} یافت نشد");
}
var image = new Domain.DevicePostImage
{
@@ -144,24 +134,22 @@ public sealed class DevicePostService : IDevicePostService
UploadedAt = DateTime.UtcNow
};
_context.DevicePostImages.Add(image);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.DevicePostImages.Add(image);
await _dbContext.SaveChangesAsync(cancellationToken);
return image.Id;
}
public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken)
{
var image = await _context.DevicePostImages
var image = await _dbContext.DevicePostImages
.FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken);
if (image == null)
{
throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد");
}
_context.DevicePostImages.Remove(image);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.DevicePostImages.Remove(image);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<bool> CanUserAccessDeviceAsync(
@@ -169,17 +157,15 @@ public sealed class DevicePostService : IDevicePostService
int deviceId,
CancellationToken cancellationToken)
{
// Check if user is the device owner or has access through DeviceUsers
var hasAccess = await _context.Devices
var hasAccess = await _dbContext.Devices
.AnyAsync(d => d.Id == deviceId && d.UserId == userId, cancellationToken);
if (!hasAccess)
{
hasAccess = await _context.DeviceUsers
hasAccess = await _dbContext.DeviceUsers
.AnyAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken);
}
return hasAccess;
}
}

View File

@@ -1,126 +1,113 @@
using AutoMapper;
using GreenHome.Application;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
public sealed class DeviceService : IDeviceService
{
private readonly GreenHomeDbContext dbContext;
private readonly IMapper mapper;
public DeviceService(GreenHomeDbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
}
public async Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken)
{
var entity = mapper.Map<Domain.Device>(dto);
dbContext.Devices.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken)
{
var items = await dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.OrderBy(d => d.DeviceName)
.ToListAsync(cancellationToken);
return mapper.Map<IReadOnlyList<DeviceDto>>(items);
}
public async Task<DeviceDto> GetDeviceId(string deviceName,CancellationToken cancellationToken)
{
var item = await dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.Where(d=>d.DeviceName==deviceName)
.FirstOrDefaultAsync(cancellationToken);
return mapper.Map<DeviceDto>(item);
}
public async Task<IReadOnlyList<DeviceDto>> GetUserDevicesAsync(int userId, CancellationToken cancellationToken)
{
var items = await dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.Where(d => d.UserId == userId)
.OrderBy(d => d.DeviceName)
.ToListAsync(cancellationToken);
return mapper.Map<IReadOnlyList<DeviceDto>>(items);
}
public async Task<PagedResult<DeviceDto>> GetDevicesAsync(DeviceFilter filter, CancellationToken cancellationToken)
{
if (!filter.UserId.HasValue)
{
throw new ArgumentException("UserId is required", nameof(filter));
}
// Get user and role
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == filter.UserId.Value, cancellationToken);
if (user == null)
{
return new PagedResult<DeviceDto>
{
Items = Array.Empty<DeviceDto>(),
TotalCount = 0,
Page = filter.Page,
PageSize = filter.PageSize
};
}
// Build query based on role
IQueryable<Domain.Device> query = dbContext.Devices
.AsNoTracking()
.Include(d => d.User);
if (user.Role == Domain.UserRole.Normal)
{
// Normal user: only own devices
query = query.Where(d => d.UserId == user.Id);
}
else if (user.Role == Domain.UserRole.Supervisor)
{
// Supervisor: devices assigned to them
query = query.Where(d => d.DeviceUsers.Any(du => du.UserId == user.Id));
}
// Admin: all devices (no filter)
// Apply search filter
if (!string.IsNullOrWhiteSpace(filter.Search))
{
var searchTerm = filter.Search.Trim().ToLower();
query = query.Where(d =>
d.DeviceName.ToLower().Contains(searchTerm) ||
d.User.Name.ToLower().Contains(searchTerm) ||
d.User.Family.ToLower().Contains(searchTerm) ||
d.Location.ToLower().Contains(searchTerm));
}
// Get total count
var totalCount = await query.CountAsync(cancellationToken);
// Apply pagination
var items = await query
.OrderBy(d => d.DeviceName)
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync(cancellationToken);
return new PagedResult<DeviceDto>
{
Items = mapper.Map<IReadOnlyList<DeviceDto>>(items),
TotalCount = totalCount,
Page = filter.Page,
PageSize = filter.PageSize
};
}
}
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure.Services.Device;
public sealed class DeviceService : IDeviceService
{
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public DeviceService(GreenHomeDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken)
{
var entity = _mapper.Map<Domain.Device>(dto);
_dbContext.Devices.Add(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken)
{
var items = await _dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.OrderBy(d => d.DeviceName)
.ToListAsync(cancellationToken);
return _mapper.Map<IReadOnlyList<DeviceDto>>(items);
}
public async Task<DeviceDto> GetDeviceId(string deviceName, CancellationToken cancellationToken)
{
var item = await _dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.Where(d => d.DeviceName == deviceName)
.FirstOrDefaultAsync(cancellationToken);
return _mapper.Map<DeviceDto>(item);
}
public async Task<IReadOnlyList<DeviceDto>> GetUserDevicesAsync(int userId, CancellationToken cancellationToken)
{
var items = await _dbContext.Devices
.AsNoTracking()
.Include(d => d.User)
.Where(d => d.UserId == userId)
.OrderBy(d => d.DeviceName)
.ToListAsync(cancellationToken);
return _mapper.Map<IReadOnlyList<DeviceDto>>(items);
}
public async Task<PagedResult<DeviceDto>> GetDevicesAsync(DeviceFilter filter, CancellationToken cancellationToken)
{
if (!filter.UserId.HasValue)
throw new ArgumentException("UserId is required", nameof(filter));
var user = await _dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == filter.UserId.Value, cancellationToken);
if (user == null)
{
return new PagedResult<DeviceDto>
{
Items = Array.Empty<DeviceDto>(),
TotalCount = 0,
Page = filter.Page,
PageSize = filter.PageSize
};
}
IQueryable<Domain.Device> query = _dbContext.Devices
.AsNoTracking()
.Include(d => d.User);
if (user.Role == Domain.UserRole.Normal)
query = query.Where(d => d.UserId == user.Id);
else if (user.Role == Domain.UserRole.Supervisor)
query = query.Where(d => d.DeviceUsers.Any(du => du.UserId == user.Id));
if (!string.IsNullOrWhiteSpace(filter.Search))
{
var searchTerm = filter.Search.Trim().ToLower();
query = query.Where(d =>
d.DeviceName.ToLower().Contains(searchTerm) ||
d.User.Name.ToLower().Contains(searchTerm) ||
d.User.Family.ToLower().Contains(searchTerm) ||
d.Location.ToLower().Contains(searchTerm));
}
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.OrderBy(d => d.DeviceName)
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync(cancellationToken);
return new PagedResult<DeviceDto>
{
Items = _mapper.Map<IReadOnlyList<DeviceDto>>(items),
TotalCount = totalCount,
Page = filter.Page,
PageSize = filter.PageSize
};
}
}

View File

@@ -1,54 +1,53 @@
using AutoMapper;
using GreenHome.Application;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
public sealed class DeviceSettingsService : IDeviceSettingsService
{
private readonly GreenHomeDbContext dbContext;
private readonly IMapper mapper;
public DeviceSettingsService(GreenHomeDbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
}
public async Task<DeviceSettingsDto?> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
{
var settings = await dbContext.DeviceSettings
.Include(x => x.Device)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.DeviceId == deviceId, cancellationToken);
return settings != null ? mapper.Map<DeviceSettingsDto>(settings) : null;
}
public async Task<int> CreateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken)
{
var entity = mapper.Map<Domain.DeviceSettings>(dto);
entity.CreatedAt = DateTime.UtcNow;
entity.UpdatedAt = DateTime.UtcNow;
dbContext.DeviceSettings.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task UpdateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken)
{
var entity = await dbContext.DeviceSettings
.FirstOrDefaultAsync(x => x.DeviceId == dto.DeviceId, cancellationToken);
if (entity == null)
{
throw new InvalidOperationException($"Device settings not found for device ID {dto.DeviceId}");
}
mapper.Map(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure.Services.Device;
public sealed class DeviceSettingsService : IDeviceSettingsService
{
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public DeviceSettingsService(GreenHomeDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<DeviceSettingsDto?> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
{
var settings = await _dbContext.DeviceSettings
.Include(x => x.Device)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.DeviceId == deviceId, cancellationToken);
return settings != null ? _mapper.Map<DeviceSettingsDto>(settings) : null;
}
public async Task<int> CreateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken)
{
var entity = _mapper.Map<Domain.DeviceSettings>(dto);
entity.CreatedAt = DateTime.UtcNow;
entity.UpdatedAt = DateTime.UtcNow;
_dbContext.DeviceSettings.Add(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task UpdateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken)
{
var entity = await _dbContext.DeviceSettings
.FirstOrDefaultAsync(x => x.DeviceId == dto.DeviceId, cancellationToken);
if (entity == null)
throw new InvalidOperationException($"Device settings not found for device ID {dto.DeviceId}");
_mapper.Map(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -1,32 +1,27 @@
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Sms.Ippanel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Device;
/// <summary>
/// سرویس مدیریت توکن و تنظیمات دستگاه
/// </summary>
public sealed class DeviceTokenService : IDeviceTokenService
{
private readonly GreenHomeDbContext dbContext;
private readonly ISmsService smsService;
private readonly ILogger<DeviceTokenService> logger;
private readonly GreenHomeDbContext _dbContext;
private readonly ISmsService _smsService;
private readonly ILogger<DeviceTokenService> _logger;
public DeviceTokenService(
GreenHomeDbContext dbContext,
ISmsService smsService,
ILogger<DeviceTokenService> logger)
{
this.dbContext = dbContext;
this.smsService = smsService;
this.logger = logger;
_dbContext = dbContext;
_smsService = smsService;
_logger = logger;
}
/// <summary>
/// دریافت فاصله زمانی آپلود بر اساس شماره تلفن یا شناسه دستگاه
/// </summary>
public async Task<GetUploadIntervalResponse> GetUploadIntervalAsync(
GetUploadIntervalRequest request,
CancellationToken cancellationToken)
@@ -35,16 +30,15 @@ public sealed class DeviceTokenService : IDeviceTokenService
{
Domain.DeviceSettings? settings = null;
// جستجو بر اساس DeviceId یا DevicePhoneNumber
if (request.DeviceId.HasValue)
{
settings = await dbContext.DeviceSettings
settings = await _dbContext.DeviceSettings
.AsNoTracking()
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId.Value, cancellationToken);
}
else if (!string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
{
settings = await dbContext.DeviceSettings
settings = await _dbContext.DeviceSettings
.AsNoTracking()
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
}
@@ -66,7 +60,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting upload interval for device");
_logger.LogError(ex, "Error getting upload interval for device");
return new GetUploadIntervalResponse
{
Success = false,
@@ -75,17 +69,13 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
}
/// <summary>
/// درخواست توکن دستگاه (تولید و ارسال کد)
/// </summary>
public async Task<RequestDeviceTokenResponse> RequestDeviceTokenAsync(
RequestDeviceTokenRequest request,
CancellationToken cancellationToken)
{
try
{
// پیدا کردن تنظیمات دستگاه بر اساس شماره تلفن
var settings = await dbContext.DeviceSettings
var settings = await _dbContext.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
if (settings == null)
@@ -97,28 +87,23 @@ public sealed class DeviceTokenService : IDeviceTokenService
};
}
// تولید کد توکن 5 رقمی
var random = new Random();
var tokenCode = random.Next(10000, 99999).ToString();
// تولید کد تایید بر اساس فرمول: (TokenCode * 7 + 12345) % 100000
var verificationCode = ((int.Parse(tokenCode) * 7 + 12345) % 100000).ToString("D5");
// ذخیره کدها
settings.TokenCode = tokenCode;
settings.VerificationCode = verificationCode;
settings.TokenExpiresAt = DateTime.UtcNow.AddMinutes(30); // اعتبار 10 دقیقه
settings.TokenExpiresAt = DateTime.UtcNow.AddMinutes(30);
settings.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
// ارسال کد توکن از طریق پیامک الگویی
try
{
await smsService.SendPatternSmsAsync(new PatternSmsRequest
await _smsService.SendPatternSmsAsync(new PatternSmsRequest
{
Recipients = new List<string> { request.DevicePhoneNumber },
PatternCode = "gfukab9r0nca0pt", // TODO: کد الگوی پیامک را اینجا قرار دهید
PatternCode = "gfukab9r0nca0pt",
Variables = new Dictionary<string, string>
{
{ "tel", "09192530212" },
@@ -128,7 +113,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
catch (Exception smsEx)
{
logger.LogError(smsEx, "Error sending token SMS to {PhoneNumber}", request.DevicePhoneNumber);
_logger.LogError(smsEx, "Error sending token SMS to {PhoneNumber}", request.DevicePhoneNumber);
return new RequestDeviceTokenResponse
{
Success = false,
@@ -136,7 +121,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
};
}
logger.LogInformation("Token requested for device phone {PhoneNumber}", request.DevicePhoneNumber);
_logger.LogInformation("Token requested for device phone {PhoneNumber}", request.DevicePhoneNumber);
return new RequestDeviceTokenResponse
{
@@ -147,7 +132,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
catch (Exception ex)
{
logger.LogError(ex, "Error requesting device token");
_logger.LogError(ex, "Error requesting device token");
return new RequestDeviceTokenResponse
{
Success = false,
@@ -156,17 +141,13 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
}
/// <summary>
/// تایید توکن دستگاه (ارسال تنظیمات)
/// </summary>
public async Task<VerifyDeviceTokenResponse> VerifyDeviceTokenAsync(
VerifyDeviceTokenRequest request,
CancellationToken cancellationToken)
{
try
{
// پیدا کردن تنظیمات دستگاه
var settings = await dbContext.DeviceSettings
var settings = await _dbContext.DeviceSettings
.Include(ds => ds.Device)
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
@@ -179,7 +160,6 @@ public sealed class DeviceTokenService : IDeviceTokenService
};
}
// بررسی انقضای توکن
if (settings.TokenExpiresAt == null || settings.TokenExpiresAt < DateTime.UtcNow)
{
return new VerifyDeviceTokenResponse
@@ -189,7 +169,6 @@ public sealed class DeviceTokenService : IDeviceTokenService
};
}
// بررسی کد تایید
if (settings.VerificationCode != request.VerificationCode)
{
return new VerifyDeviceTokenResponse
@@ -198,21 +177,20 @@ public sealed class DeviceTokenService : IDeviceTokenService
Message = "کد تایید نادرست است"
};
}
Random rnd = new Random();
// آماده‌سازی پارامترها برای ارسال پیامک الگویی
var rnd = new Random();
var deviceId = settings.Device.Id + "01";
var uploadInterval = rnd.Next(1, 9) + "" + settings.UploadIntervalMin;
var smsInterval = settings.MinimumSmsIntervalMinutes > 0 ? "1" + settings.MinimumSmsIntervalMinutes : "0" + rnd.Next(1, 9);
var sysNumber = rnd.Next(139, 97654); // عدد رندوم SysNumber
var simType = rnd.Next(1, 9).ToString() + ((int?)settings.SimCardType)?.ToString() ?? "1"; // مقدار عددی enum
var sysNumber = rnd.Next(139, 97654);
var simType = rnd.Next(1, 9).ToString() + ((int?)settings.SimCardType)?.ToString() ?? "1";
// ارسال تنظیمات از طریق پیامک الگویی
try
{
await smsService.SendPatternSmsAsync(new PatternSmsRequest
await _smsService.SendPatternSmsAsync(new PatternSmsRequest
{
Recipients = new List<string> { request.DevicePhoneNumber },
PatternCode = "kx3kfqri7g09r02", // TODO: کد الگوی پیامک را اینجا قرار دهید
PatternCode = "kx3kfqri7g09r02",
Variables = new Dictionary<string, string>
{
{ "deviceId", deviceId },
@@ -225,7 +203,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
catch (Exception smsEx)
{
logger.LogError(smsEx, "Error sending settings SMS to {PhoneNumber}", request.DevicePhoneNumber);
_logger.LogError(smsEx, "Error sending settings SMS to {PhoneNumber}", request.DevicePhoneNumber);
return new VerifyDeviceTokenResponse
{
Success = false,
@@ -233,14 +211,13 @@ public sealed class DeviceTokenService : IDeviceTokenService
};
}
// پاک کردن کدها بعد از استفاده موفق
settings.TokenCode = null;
settings.VerificationCode = null;
settings.TokenExpiresAt = null;
settings.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("Device token verified for {PhoneNumber}", request.DevicePhoneNumber);
_logger.LogInformation("Device token verified for {PhoneNumber}", request.DevicePhoneNumber);
return new VerifyDeviceTokenResponse
{
@@ -251,7 +228,7 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
catch (Exception ex)
{
logger.LogError(ex, "Error verifying device token");
_logger.LogError(ex, "Error verifying device token");
return new VerifyDeviceTokenResponse
{
Success = false,
@@ -260,4 +237,3 @@ public sealed class DeviceTokenService : IDeviceTokenService
}
}
}

View File

@@ -2,14 +2,16 @@ using System.Diagnostics;
using System.Text;
using GreenHome.AI.DeepSeek;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Report;
public class DailyReportService : IDailyReportService
public sealed class DailyReportService : IDailyReportService
{
private readonly GreenHomeDbContext _context;
private readonly GreenHomeDbContext _dbContext;
private readonly IDeepSeekService _deepSeekService;
private readonly ILogger<DailyReportService> _logger;
@@ -18,7 +20,7 @@ public class DailyReportService : IDailyReportService
IDeepSeekService deepSeekService,
ILogger<DailyReportService> logger)
{
_context = context;
_dbContext = context;
_deepSeekService = deepSeekService;
_logger = logger;
}
@@ -27,14 +29,10 @@ public class DailyReportService : IDailyReportService
DailyReportRequest request,
CancellationToken cancellationToken)
{
// Validate Persian date format
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
{
if (!PersianDateHelper.TryParsePersianDate(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
var existingReport = await _dbContext.DailyReports
.Include(r => r.Device)
.FirstOrDefaultAsync(
r => r.DeviceId == request.DeviceId && r.PersianDate == request.PersianDate,
@@ -61,21 +59,16 @@ public class DailyReportService : IDailyReportService
};
}
// Get device info
var device = await _context.Devices
var device = await _dbContext.Devices
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings (including ProductType if available)
var deviceSettings = await _context.DeviceSettings
var deviceSettings = await _dbContext.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Query telemetry data for the specified date
var telemetryRecords = await _context.TelemetryRecords
var telemetryRecords = await _dbContext.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
.OrderBy(t => t.TimestampUtc)
.Select(t => new
@@ -89,12 +82,9 @@ public class DailyReportService : IDailyReportService
.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)
@@ -105,24 +95,21 @@ public class DailyReportService : IDailyReportService
"تعداد {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)
var localTime = record.TimestampUtc.AddHours(3.5);
dataBuilder.AppendLine(
$"{localTime:HH:mm:ss} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
}
// Prepare the question for AI
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
var areaInfo = deviceSettings?.AreaSquareMeters != null
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
: string.Empty;
@@ -139,16 +126,15 @@ public class DailyReportService : IDailyReportService
خلاصه و مفید باش (حداکثر 300 کلمه).";
// Send to DeepSeek
var stopwatch = Stopwatch.StartNew();
ChatResponse? aiResponse;
try
{
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی."
: "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی.";
var chatRequest = new ChatRequest
{
Model = "deepseek-chat",
@@ -170,15 +156,12 @@ public class DailyReportService : IDailyReportService
stopwatch.Stop();
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
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,
@@ -197,8 +180,8 @@ public class DailyReportService : IDailyReportService
CreatedAt = DateTime.UtcNow
};
_context.DailyReports.Add(dailyReport);
await _context.SaveChangesAsync(cancellationToken);
_dbContext.DailyReports.Add(dailyReport);
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"گزارش روزانه جدید برای دستگاه {DeviceId} و تاریخ {Date} ایجاد شد. توکن مصرف شده: {Tokens}",
@@ -219,49 +202,21 @@ public class DailyReportService : IDailyReportService
};
}
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
{
year = month = day = 0;
if (string.IsNullOrWhiteSpace(persianDate))
return false;
var parts = persianDate.Split('/');
if (parts.Length != 3)
return false;
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
return false;
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
return false;
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
return false;
return true;
}
public async Task<DailyReportResponse> GetWeeklyAnalysisAsync(
WeeklyAnalysisRequest request,
CancellationToken cancellationToken)
{
// Get device info
var device = await _context.Devices
var device = await _dbContext.Devices
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings
var deviceSettings = await _context.DeviceSettings
var deviceSettings = await _dbContext.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Query telemetry data for the week
var telemetryRecords = await _context.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId &&
var telemetryRecords = await _dbContext.TelemetryRecords
.Where(t => t.DeviceId == request.DeviceId &&
string.Compare(t.PersianDate, request.StartDate) >= 0 &&
string.Compare(t.PersianDate, request.EndDate) <= 0)
.OrderBy(t => t.TimestampUtc)
@@ -277,12 +232,9 @@ public class DailyReportService : IDailyReportService
.ToListAsync(cancellationToken);
if (telemetryRecords.Count == 0)
{
throw new InvalidOperationException(
$"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد");
}
// Sample 1 per 100 records
var sampledRecords = telemetryRecords
.Select((record, index) => new { record, index })
.Where(x => x.index % 100 == 0)
@@ -293,7 +245,6 @@ public class DailyReportService : IDailyReportService
"تعداد {TotalCount} رکورد یافت شد. نمونه‌برداری هفتگی: {SampledCount} رکورد",
telemetryRecords.Count, sampledRecords.Count);
// Build the data string
var dataBuilder = new StringBuilder();
dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------");
@@ -305,7 +256,6 @@ public class DailyReportService : IDailyReportService
$"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
}
// Prepare AI prompt
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
@@ -330,7 +280,6 @@ public class DailyReportService : IDailyReportService
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و داده‌های تلمتری رو تحلیل می‌کنی."
: "تو یک متخصص کشاورزی و گلخانه هستی که داده‌های تلمتری رو تحلیل می‌کنی.";
// Send to DeepSeek
var stopwatch = Stopwatch.StartNew();
var chatRequest = new ChatRequest
{
@@ -348,9 +297,7 @@ public class DailyReportService : IDailyReportService
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
{
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
}
return new DailyReportResponse
{
@@ -371,21 +318,16 @@ public class DailyReportService : IDailyReportService
MonthlyAnalysisRequest request,
CancellationToken cancellationToken)
{
// Get device info
var device = await _context.Devices
var device = await _dbContext.Devices
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
}
// Get device settings
var deviceSettings = await _context.DeviceSettings
var deviceSettings = await _dbContext.DeviceSettings
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
// Get all daily reports for this month
var dailyReports = await _context.DailyReports
var dailyReports = await _dbContext.DailyReports
.Where(dr => dr.DeviceId == request.DeviceId &&
dr.PersianYear == request.Year &&
dr.PersianMonth == request.Month)
@@ -394,12 +336,9 @@ public class DailyReportService : IDailyReportService
.ToListAsync(cancellationToken);
if (dailyReports.Count == 0)
{
throw new InvalidOperationException(
$"هیچ تحلیل روزانه‌ای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد");
}
// Build summary of daily analyses
var summaryBuilder = new StringBuilder();
summaryBuilder.AppendLine($"تحلیل‌های روزانه ماه {request.Month} سال {request.Year}:");
summaryBuilder.AppendLine();
@@ -413,7 +352,6 @@ public class DailyReportService : IDailyReportService
summaryBuilder.AppendLine();
}
// Prepare AI prompt
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
? $" محصول کشت شده: {deviceSettings.ProductType}."
: string.Empty;
@@ -439,7 +377,6 @@ public class DailyReportService : IDailyReportService
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده."
: "تو یک متخصص کشاورزی و گلخانه هستی. تحلیل‌های روزانه رو بررسی کن و یک جمع‌بندی ماهانه جامع ارائه بده.";
// Send to DeepSeek
var stopwatch = Stopwatch.StartNew();
var chatRequest = new ChatRequest
{
@@ -457,9 +394,7 @@ public class DailyReportService : IDailyReportService
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
{
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
}
return new DailyReportResponse
{
@@ -476,4 +411,3 @@ public class DailyReportService : IDailyReportService
};
}
}

View File

@@ -1,15 +1,16 @@
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Report;
public sealed class MonthlyReportService : IMonthlyReportService
{
private readonly GreenHomeDbContext _context;
private readonly GreenHomeDbContext _dbContext;
public MonthlyReportService(GreenHomeDbContext context)
{
_context = context;
_dbContext = context;
}
public async Task<MonthlyReportDto> GetMonthlyReportAsync(
@@ -18,16 +19,13 @@ public sealed class MonthlyReportService : IMonthlyReportService
int month,
CancellationToken cancellationToken)
{
var device = await _context.Devices
var device = await _dbContext.Devices
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
if (device == null)
{
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
}
// Get alert logs for the month
var alertLogs = await _context.AlertLogs
var alertLogs = await _dbContext.AlertLogs
.Where(al => al.DeviceId == deviceId &&
al.SentAt.Year == year &&
al.SentAt.Month == month)
@@ -40,8 +38,7 @@ public sealed class MonthlyReportService : IMonthlyReportService
var failedAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Failed);
var powerOutageAlerts = alertLogs.Count(al => al.AlertType == Domain.AlertType.PowerOutage);
// Get telemetry statistics
var telemetryData = await _context.TelemetryRecords
var telemetryData = await _dbContext.TelemetryRecords
.Where(t => t.DeviceId == deviceId &&
t.PersianYear == year &&
t.PersianMonth == month)
@@ -58,35 +55,26 @@ public sealed class MonthlyReportService : IMonthlyReportService
var avgGas = telemetryData.Any() ? (int)telemetryData.Average(t => t.GasPPM) : 0;
var maxGas = telemetryData.Any() ? telemetryData.Max(t => t.GasPPM) : 0;
// Get user activity
var userReportsCount = await _context.UserDailyReports
var userReportsCount = await _dbContext.UserDailyReports
.CountAsync(r => r.DeviceId == deviceId &&
r.PersianYear == year &&
r.PersianMonth == month,
cancellationToken);
var checklistCompletionsCount = await _context.ChecklistCompletions
var checklistCompletionsCount = await _dbContext.ChecklistCompletions
.Where(cc => cc.Checklist.DeviceId == deviceId)
.CountAsync(cc => cc.PersianDate.StartsWith($"{year}/{month:D2}"), cancellationToken);
var dailyAnalysesCount = await _context.DailyReports
var dailyAnalysesCount = await _dbContext.DailyReports
.CountAsync(dr => dr.DeviceId == deviceId &&
dr.PersianYear == year &&
dr.PersianMonth == month,
cancellationToken);
// Generate performance summary
var performanceSummary = GeneratePerformanceSummary(
totalTelemetryRecords,
totalAlerts,
successfulAlerts,
failedAlerts,
avgTemp,
avgHumidity,
avgLux,
avgGas,
userReportsCount,
checklistCompletionsCount);
totalTelemetryRecords, totalAlerts, successfulAlerts, failedAlerts,
avgTemp, avgHumidity, avgLux, avgGas,
userReportsCount, checklistCompletionsCount);
return new MonthlyReportDto
{
@@ -118,17 +106,10 @@ public sealed class MonthlyReportService : IMonthlyReportService
};
}
private string GeneratePerformanceSummary(
int totalRecords,
int totalAlerts,
int successfulAlerts,
int failedAlerts,
decimal avgTemp,
decimal avgHumidity,
decimal avgLux,
int avgGas,
int userReports,
int checklistCompletions)
private static string GeneratePerformanceSummary(
int totalRecords, int totalAlerts, int successfulAlerts, int failedAlerts,
decimal avgTemp, decimal avgHumidity, decimal avgLux, int avgGas,
int userReports, int checklistCompletions)
{
var summary = new List<string>();
@@ -157,7 +138,6 @@ public sealed class MonthlyReportService : IMonthlyReportService
summary.Add($" • تکمیل چک‌لیست: {checklistCompletions}");
}
// Performance rating
summary.Add("");
var rating = CalculatePerformanceRating(totalRecords, failedAlerts, totalAlerts);
summary.Add($"⭐ ارزیابی کلی: {rating}");
@@ -165,7 +145,7 @@ public sealed class MonthlyReportService : IMonthlyReportService
return string.Join("\n", summary);
}
private string CalculatePerformanceRating(int totalRecords, int failedAlerts, int totalAlerts)
private static string CalculatePerformanceRating(int totalRecords, int failedAlerts, int totalAlerts)
{
if (totalRecords == 0)
return "بدون داده";
@@ -182,4 +162,3 @@ public sealed class MonthlyReportService : IMonthlyReportService
return "نیاز به بررسی 🔧";
}
}

View File

@@ -1,8 +1,10 @@
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Report;
public sealed class UserDailyReportService : IUserDailyReportService
{
@@ -87,8 +89,8 @@ public sealed class UserDailyReportService : IUserDailyReportService
CreateUserDailyReportRequest request,
CancellationToken cancellationToken)
{
// Validate date format
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
// Validate Persian date format
if (!PersianDateHelper.TryParsePersianDate(request.PersianDate, out var year, out var month, out var day))
{
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد");
}
@@ -198,28 +200,4 @@ public sealed class UserDailyReportService : IUserDailyReportService
_context.ReportImages.Remove(image);
await _context.SaveChangesAsync(cancellationToken);
}
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

@@ -1,6 +1,6 @@
using GreenHome.Application;
namespace GreenHome.Infrastructure;
namespace GreenHome.Infrastructure.Services.Sun;
/// <summary>
/// سرویس محاسبه طلوع و غروب خورشید
@@ -11,10 +11,10 @@ public sealed class SunCalculatorService : ISunCalculatorService
{
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;
@@ -28,33 +28,33 @@ public sealed class SunCalculatorService : ISunCalculatorService
// 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))
@@ -62,64 +62,63 @@ public sealed class SunCalculatorService : ISunCalculatorService
+ 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)
private static 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)
private static double ToRadians(double degrees)
{
return degrees * Math.PI / 180.0;
}
private double ToDegrees(double radians)
private static double ToDegrees(double radians)
{
return radians * 180.0 / Math.PI;
}
}

View File

@@ -1,171 +1,163 @@
using System.Globalization;
using AutoMapper;
using GreenHome.Application;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure;
public sealed class TelemetryService : ITelemetryService
{
private readonly GreenHomeDbContext dbContext;
private readonly IMapper mapper;
private static readonly PersianCalendar PersianCalendar = new PersianCalendar();
public TelemetryService(GreenHomeDbContext dbContext, IMapper mapper)
{
this.dbContext = dbContext;
this.mapper = mapper;
}
public async Task<int> AddAsync(TelemetryDto dto, CancellationToken cancellationToken)
{
var entity = mapper.Map<Domain.TelemetryRecord>(dto);
if (!string.IsNullOrEmpty(dto.DeviceName))
{
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.DeviceName == dto.DeviceName, cancellationToken);
if (device != null)
{
entity.DeviceId = device.Id;
dto.DeviceId = device.Id; // Update DTO for alert service
}
}
if (dto.Id!=0)
{
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.Id == dto.Id, cancellationToken);
if (device != null)
{
entity.DeviceId = device.Id;
}
}
// ذخیره زمان سرور در لحظه ثبت
entity.ServerTimestampUtc = DateTime.Now;
var dt = dto.TimestampUtc!.Value.ToLocalTime();
var py = PersianCalendar.GetYear(dt);
var pm = PersianCalendar.GetMonth(dt);
var pd = PersianCalendar.GetDayOfMonth(dt);
entity.PersianYear = py;
entity.PersianMonth = pm;
entity.PersianDate = $"{py:0000}/{pm:00}/{pd:00}";
if(entity.Lux < 0)
{
var lastItem = await dbContext.TelemetryRecords
.Where(x => x.DeviceId == entity.DeviceId)
.OrderByDescending(x => x.TimestampUtc)
.FirstOrDefaultAsync(cancellationToken);
entity.Lux = lastItem?.Lux ?? 0;
}
dbContext.TelemetryRecords.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task<PagedResult<TelemetryDto>> ListAsync(TelemetryFilter filter, CancellationToken cancellationToken)
{
var query = dbContext.TelemetryRecords.AsNoTracking().AsQueryable();
if (filter.DeviceId.HasValue)
{
query = query.Where(x => x.DeviceId == filter.DeviceId.Value);
}
if (filter.StartDateUtc.HasValue)
{
//var start = filter.StartDateUtc.Value.Date.AddDays(1);
query = query.Where(x => x.ServerTimestampUtc >= filter.StartDateUtc.Value.ToLocalTime());
}
if (filter.EndDateUtc.HasValue)
{
query = query.Where(x => x.ServerTimestampUtc < filter.EndDateUtc.Value.ToLocalTime());
}
if(filter.Page <= 0) filter.Page = 1;
if(filter.PageSize <= 0) filter.PageSize = 1000000; // No limit
var total = await query.CountAsync(cancellationToken);
var skip = (filter.Page - 1) * filter.PageSize;
var items = await query
.OrderByDescending(x => x.ServerTimestampUtc)
.Skip(skip)
.Take(filter.PageSize)
.ToListAsync(cancellationToken);
if(items.Any(x => x.Lux < 0))
{
for (int i = 1; i < items.Count; i++)
{
if (items[i].Lux < 0)
items[i].Lux = items[i - 1].Lux;
}
}
return new PagedResult<TelemetryDto>
{
Items = mapper.Map<IReadOnlyList<TelemetryDto>>(items),
TotalCount = total,
Page = filter.Page,
PageSize = filter.PageSize
};
}
public async Task<TelemetryMinMax> GetMinMaxAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, CancellationToken cancellationToken)
{
var query = dbContext.TelemetryRecords.AsNoTracking().Where(x => x.DeviceId == deviceId);
if (startUtc.HasValue) query = query.Where(x => x.TimestampUtc >= startUtc.Value);
if (endUtc.HasValue) query = query.Where(x => x.TimestampUtc <= endUtc.Value);
var result = await query
.Select(x => new
{
x.TemperatureC,
x.HumidityPercent,
x.SoilPercent,
x.GasPPM,
x.Lux
})
.ToListAsync(cancellationToken);
if (result.Count == 0)
{
return new TelemetryMinMax();
}
return new TelemetryMinMax
{
MinTemperatureC = result.Min(x => x.TemperatureC),
MaxTemperatureC = result.Max(x => x.TemperatureC),
MinHumidityPercent = result.Min(x => x.HumidityPercent),
MaxHumidityPercent = result.Max(x => x.HumidityPercent),
MinSoilPercent = result.Min(x => x.SoilPercent),
MaxSoilPercent = result.Max(x => x.SoilPercent),
MinGasPPM = result.Min(x => x.GasPPM),
MaxGasPPM = result.Max(x => x.GasPPM),
MinLux = result.Min(x => x.Lux),
MaxLux = result.Max(x => x.Lux)
};
}
public async Task<IReadOnlyList<DayCount>> GetMonthDaysAsync(int deviceId, int persianYear, int persianMonth, CancellationToken cancellationToken)
{
var query = dbContext.TelemetryRecords.AsNoTracking()
.Where(x => x.DeviceId == deviceId && x.PersianYear == persianYear && x.PersianMonth == persianMonth)
.GroupBy(x => x.PersianDate)
.Select(g => new DayCount { PersianDate = g.Key, Count = g.Count() })
.OrderBy(x => x.PersianDate);
return await query.ToListAsync(cancellationToken);
}
public async Task<IReadOnlyList<int>> GetActiveMonthsAsync(int deviceId, int persianYear, CancellationToken cancellationToken)
{
var months = await dbContext.TelemetryRecords.AsNoTracking()
.Where(x => x.DeviceId == deviceId && x.PersianYear == persianYear)
.Select(x => x.PersianMonth)
.Distinct()
.OrderBy(x => x)
.ToListAsync(cancellationToken);
return months;
}
}
using AutoMapper;
using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure.Services.Telemetry;
public sealed class TelemetryService : ITelemetryService
{
private readonly GreenHomeDbContext _dbContext;
private readonly IMapper _mapper;
public TelemetryService(GreenHomeDbContext dbContext, IMapper mapper)
{
_dbContext = dbContext;
_mapper = mapper;
}
public async Task<int> AddAsync(TelemetryDto dto, CancellationToken cancellationToken)
{
var entity = _mapper.Map<Domain.TelemetryRecord>(dto);
if (!string.IsNullOrEmpty(dto.DeviceName))
{
var device = await _dbContext.Devices.FirstOrDefaultAsync(d => d.DeviceName == dto.DeviceName, cancellationToken);
if (device != null)
{
entity.DeviceId = device.Id;
dto.DeviceId = device.Id;
}
}
if (dto.Id != 0)
{
var device = await _dbContext.Devices.FirstOrDefaultAsync(d => d.Id == dto.Id, cancellationToken);
if (device != null)
entity.DeviceId = device.Id;
}
entity.ServerTimestampUtc = DateTime.Now;
if (dto.TimestampUtc.HasValue)
{
var (year, month, day, persianDate) = dto.TimestampUtc.Value.ToPersianDate();
entity.PersianYear = year;
entity.PersianMonth = month;
entity.PersianDate = persianDate;
}
if (entity.Lux < 0)
{
var lastItem = await _dbContext.TelemetryRecords
.Where(x => x.DeviceId == entity.DeviceId)
.OrderByDescending(x => x.TimestampUtc)
.FirstOrDefaultAsync(cancellationToken);
entity.Lux = lastItem?.Lux ?? 0;
}
_dbContext.TelemetryRecords.Add(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
}
public async Task<PagedResult<TelemetryDto>> ListAsync(TelemetryFilter filter, CancellationToken cancellationToken)
{
var query = _dbContext.TelemetryRecords.AsNoTracking().AsQueryable();
if (filter.DeviceId.HasValue)
query = query.Where(x => x.DeviceId == filter.DeviceId.Value);
if (filter.StartDateUtc.HasValue)
query = query.Where(x => x.ServerTimestampUtc >= filter.StartDateUtc.Value.ToLocalTime());
if (filter.EndDateUtc.HasValue)
query = query.Where(x => x.ServerTimestampUtc < filter.EndDateUtc.Value.ToLocalTime());
if (filter.Page <= 0) filter.Page = 1;
if (filter.PageSize <= 0) filter.PageSize = 1000000;
var total = await query.CountAsync(cancellationToken);
var skip = (filter.Page - 1) * filter.PageSize;
var items = await query
.OrderByDescending(x => x.ServerTimestampUtc)
.Skip(skip)
.Take(filter.PageSize)
.ToListAsync(cancellationToken);
// Fix negative Lux values
for (int i = 1; i < items.Count; i++)
{
if (items[i].Lux < 0)
items[i].Lux = items[i - 1].Lux;
}
return new PagedResult<TelemetryDto>
{
Items = _mapper.Map<IReadOnlyList<TelemetryDto>>(items),
TotalCount = total,
Page = filter.Page,
PageSize = filter.PageSize
};
}
public async Task<TelemetryMinMax> GetMinMaxAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, CancellationToken cancellationToken)
{
var query = _dbContext.TelemetryRecords.AsNoTracking().Where(x => x.DeviceId == deviceId);
if (startUtc.HasValue) query = query.Where(x => x.TimestampUtc >= startUtc.Value);
if (endUtc.HasValue) query = query.Where(x => x.TimestampUtc <= endUtc.Value);
var result = await query
.Select(x => new
{
x.TemperatureC,
x.HumidityPercent,
x.SoilPercent,
x.GasPPM,
x.Lux
})
.ToListAsync(cancellationToken);
if (result.Count == 0)
return new TelemetryMinMax();
return new TelemetryMinMax
{
MinTemperatureC = result.Min(x => x.TemperatureC),
MaxTemperatureC = result.Max(x => x.TemperatureC),
MinHumidityPercent = result.Min(x => x.HumidityPercent),
MaxHumidityPercent = result.Max(x => x.HumidityPercent),
MinSoilPercent = result.Min(x => x.SoilPercent),
MaxSoilPercent = result.Max(x => x.SoilPercent),
MinGasPPM = result.Min(x => x.GasPPM),
MaxGasPPM = result.Max(x => x.GasPPM),
MinLux = result.Min(x => x.Lux),
MaxLux = result.Max(x => x.Lux)
};
}
public async Task<IReadOnlyList<DayCount>> GetMonthDaysAsync(int deviceId, int persianYear, int persianMonth, CancellationToken cancellationToken)
{
var query = _dbContext.TelemetryRecords.AsNoTracking()
.Where(x => x.DeviceId == deviceId && x.PersianYear == persianYear && x.PersianMonth == persianMonth)
.GroupBy(x => x.PersianDate)
.Select(g => new DayCount { PersianDate = g.Key, Count = g.Count() })
.OrderBy(x => x.PersianDate);
return await query.ToListAsync(cancellationToken);
}
public async Task<IReadOnlyList<int>> GetActiveMonthsAsync(int deviceId, int persianYear, CancellationToken cancellationToken)
{
var months = await _dbContext.TelemetryRecords.AsNoTracking()
.Where(x => x.DeviceId == deviceId && x.PersianYear == persianYear)
.Select(x => x.PersianMonth)
.Distinct()
.OrderBy(x => x)
.ToListAsync(cancellationToken);
return months;
}
}

View File

@@ -1,4 +0,0 @@
public class VoiceCallService
{
}