From dca3fc62ca2396474fee14b2df3a311b6bb3f59b Mon Sep 17 00:00:00 2001 From: alireza Date: Tue, 28 Apr 2026 09:36:29 +0330 Subject: [PATCH] add RelayConditions endpoint and RelayCount to DeviceSettings and bugfix --- .../Controllers/AlertConditionsController.cs | 10 + .../Controllers/TelemetryController.cs | 61 +- src/GreenHome.Application/Dtos.cs | 23 + .../IAlertConditionService.cs | 13 + src/GreenHome.Domain/DeviceSettings.cs | 5 + .../AlertConditionService.cs | 55 + src/GreenHome.Infrastructure/AlertService.cs | 20 +- .../GreenHomeDbContext.cs | 1 + ..._AddRelayCountToDeviceSettings.Designer.cs | 1251 +++++++++++++++++ ...428054608_AddRelayCountToDeviceSettings.cs | 29 + .../GreenHomeDbContextModelSnapshot.cs | 5 + 11 files changed, 1445 insertions(+), 28 deletions(-) create mode 100644 src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.Designer.cs create mode 100644 src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.cs diff --git a/src/GreenHome.Api/Controllers/AlertConditionsController.cs b/src/GreenHome.Api/Controllers/AlertConditionsController.cs index ef0593e..bf85bf9 100644 --- a/src/GreenHome.Api/Controllers/AlertConditionsController.cs +++ b/src/GreenHome.Api/Controllers/AlertConditionsController.cs @@ -81,5 +81,15 @@ public class AlertConditionsController : ControllerBase } return Ok(); } + + /// + /// دریافت شرایط هشدار رله‌ای (notificationType > 0) برای یک دستگاه + /// + [HttpGet("relay/{deviceId}")] + public async Task> GetRelayConditions(int deviceId, CancellationToken cancellationToken) + { + var conditions = await alertConditionService.GetRelayConditionsAsync(deviceId, cancellationToken); + return Ok(new { conditions }); + } } diff --git a/src/GreenHome.Api/Controllers/TelemetryController.cs b/src/GreenHome.Api/Controllers/TelemetryController.cs index f009a97..f519120 100644 --- a/src/GreenHome.Api/Controllers/TelemetryController.cs +++ b/src/GreenHome.Api/Controllers/TelemetryController.cs @@ -9,11 +9,13 @@ public class TelemetryController : ControllerBase { private readonly ITelemetryService telemetryService; private readonly IAlertService alertService; + private readonly IAlertConditionService alertConditionService; - public TelemetryController(ITelemetryService telemetryService, IAlertService alertService) + public TelemetryController(ITelemetryService telemetryService, IAlertService alertService, IAlertConditionService alertConditionService) { this.telemetryService = telemetryService; this.alertService = alertService; + this.alertConditionService = alertConditionService; } [HttpGet] @@ -25,7 +27,7 @@ public class TelemetryController : ControllerBase } [HttpGet("AddData")] - public async Task> Create([FromQuery] TelemetryDto telementry, CancellationToken cancellationToken) + public async Task> Create([FromQuery] TelemetryDto telementry, [FromQuery] string? lastUpdateDate, CancellationToken cancellationToken) { telementry.TimestampUtc = DateTime.UtcNow; @@ -43,28 +45,47 @@ public class TelemetryController : ControllerBase } } - // Check and send alerts if needed (fire and forget) - // _ = Task.Run(async () => - // { - // try - // { - // // Get deviceId from the saved telemetry record - // var deviceId = telementry.DeviceId; - // if (deviceId > 0) - // { - // await alertService.CheckAndSendAlertsAsync(deviceId, telementry, cancellationToken); - // } - // } - // catch - // { - // // Log error but don't fail the request - // // Errors are logged in AlertService - // } - // }, cancellationToken); + // Check if relay conditions have been updated since lastUpdateDate + if (deviceId > 0 && !string.IsNullOrEmpty(lastUpdateDate)) + { + var parsedDate = ParseLastUpdateDate(lastUpdateDate); + // var relayConditions = await alertConditionService.GetUpdatedRelayConditionsAsync(deviceId, parsedDate, cancellationToken); + // if (relayConditions.Count > 0) + // { + // return Ok(new { conditions = relayConditions }); + // } + + var hasUpdates = await alertConditionService.HasUpdatedConditionsAsync(deviceId, parsedDate, cancellationToken); + if(hasUpdates) + return Ok("Relay"); + } return Ok(id + ""); } + /// + /// Parse date string in format: yyyy/MM/dd-HH:mm + /// + private static DateTime? ParseLastUpdateDate(string dateStr) + { + if (string.IsNullOrEmpty(dateStr)) + return null; + + // Try parsing as yyyy/MM/dd-HH:mm + if (DateTime.TryParseExact(dateStr, "yyyy/MM/dd-HH:mm", null, System.Globalization.DateTimeStyles.None, out var result)) + { + return DateTime.SpecifyKind(result, DateTimeKind.Utc); + } + + // Try parsing as yyyy-MM-dd HH:mm (ISO-like) + if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out var result2)) + { + return DateTime.SpecifyKind(result2, DateTimeKind.Utc); + } + + return null; + } + [HttpGet("minmax")] public async Task> MinMax([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, CancellationToken cancellationToken) { diff --git a/src/GreenHome.Application/Dtos.cs b/src/GreenHome.Application/Dtos.cs index 66f4516..c7e05bb 100644 --- a/src/GreenHome.Application/Dtos.cs +++ b/src/GreenHome.Application/Dtos.cs @@ -136,6 +136,7 @@ public sealed class DeviceSettingsDto public int UploadIntervalMin { get; set; } = 5; public string DevicePhoneNumber { get; set; } = string.Empty; public Domain.SimCardType? SimCardType { get; set; } + public int RelayCount { get; set; } = 4; public string SmsRecievers { get; set; } = string.Empty; public string? TokenCode { get; set; } public string? VerificationCode { get; set; } @@ -171,6 +172,28 @@ public sealed class AlertRuleDto public int Order { get; set; } } +/// +/// DTO for relay alert conditions (notificationType > 0) +/// +public sealed class RelayConditionDto +{ + public int Enabled { get; set; } + public int TimeType { get; set; } + public string Relay { get; set; } = string.Empty; + public List Rules { get; set; } = new(); +} + +/// +/// DTO for relay condition rules +/// +public sealed class RelayRuleDto +{ + public string Sensor { get; set; } = string.Empty; + public string Op { get; set; } = string.Empty; + public decimal V1 { get; set; } + public decimal? V2 { get; set; } +} + public sealed class CreateAlertConditionRequest { public required int DeviceId { get; set; } diff --git a/src/GreenHome.Application/IAlertConditionService.cs b/src/GreenHome.Application/IAlertConditionService.cs index 4b4ea09..c532eda 100644 --- a/src/GreenHome.Application/IAlertConditionService.cs +++ b/src/GreenHome.Application/IAlertConditionService.cs @@ -8,5 +8,18 @@ public interface IAlertConditionService Task UpdateAsync(UpdateAlertConditionRequest request, CancellationToken cancellationToken); Task DeleteAsync(int id, CancellationToken cancellationToken); Task ToggleEnabledAsync(int id, bool isEnabled, CancellationToken cancellationToken); + + /// + /// دریافت شرایط هشدار رله‌ای (notificationType > 0) برای یک دستگاه + /// + Task> GetRelayConditionsAsync(int deviceId, CancellationToken cancellationToken); + + /// + /// دریافت شرایط هشدار رله‌ای که بعد از تاریخ مشخصی به‌روزرسانی شده‌اند + /// اگر هیچ رکوردی به‌روزرسانی نشده باشد، لیست خالی برمی‌گرداند + /// + Task> GetUpdatedRelayConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken); + + Task HasUpdatedConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken); } diff --git a/src/GreenHome.Domain/DeviceSettings.cs b/src/GreenHome.Domain/DeviceSettings.cs index 3244c59..8bb7eae 100644 --- a/src/GreenHome.Domain/DeviceSettings.cs +++ b/src/GreenHome.Domain/DeviceSettings.cs @@ -84,6 +84,11 @@ public sealed class DeviceSettings /// public SimCardType? SimCardType { get; set; } + /// + /// تعداد رله‌ها + /// + public int RelayCount { get; set; } = 4; + [StringLength(120)] public string SmsRecievers { get; set; } = string.Empty; diff --git a/src/GreenHome.Infrastructure/AlertConditionService.cs b/src/GreenHome.Infrastructure/AlertConditionService.cs index 095acf8..c09ba7f 100644 --- a/src/GreenHome.Infrastructure/AlertConditionService.cs +++ b/src/GreenHome.Infrastructure/AlertConditionService.cs @@ -127,5 +127,60 @@ public sealed class AlertConditionService : IAlertConditionService await dbContext.SaveChangesAsync(cancellationToken); return true; } + + public async Task> GetRelayConditionsAsync(int deviceId, CancellationToken cancellationToken) + { + var conditions = await dbContext.AlertConditions + .Include(x => x.Rules) + .Where(x => x.DeviceId == deviceId && (int)x.NotificationType > 0) + .OrderByDescending(x => x.CreatedAt) + .AsNoTracking() + .ToListAsync(cancellationToken); + + var result = conditions.Select(c => new RelayConditionDto + { + Enabled = c.IsEnabled ? 1 : 0, + TimeType = (int)c.TimeType, + Relay = "R" + (int)c.NotificationType, + Rules = c.Rules.OrderBy(r => r.Order).Select(r => new RelayRuleDto + { + Sensor = ((int)r.SensorType).ToString(), + Op = ((int)r.ComparisonType).ToString(), + V1 = r.Value1, + V2 = r.Value2 + }).ToList() + }).ToList(); + + return result; + } + + public async Task> GetUpdatedRelayConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken) + { + // اگر lastUpdateDate null باشه، یعنی کلاینت تا حالا هیچ داده‌ای نگرفته، همه رو برگردون + if (lastUpdateDate == null) + { + return await GetRelayConditionsAsync(deviceId, cancellationToken); + } + + var hasUpdates = await dbContext.AlertConditions + .AnyAsync(x => x.DeviceId == deviceId && (int)x.NotificationType > 0 && x.UpdatedAt > lastUpdateDate, cancellationToken); + + if (!hasUpdates) + { + return Array.Empty(); + } + + return await GetRelayConditionsAsync(deviceId, cancellationToken); + } + + public async Task HasUpdatedConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken) + { + if(lastUpdateDate == null) return true; + + var hasUpdates = await dbContext.AlertConditions + .AnyAsync(x => x.DeviceId == deviceId && (int)x.NotificationType > 0 && x.UpdatedAt > lastUpdateDate, cancellationToken); + + return hasUpdates; + } } diff --git a/src/GreenHome.Infrastructure/AlertService.cs b/src/GreenHome.Infrastructure/AlertService.cs index 7016ded..cac755b 100644 --- a/src/GreenHome.Infrastructure/AlertService.cs +++ b/src/GreenHome.Infrastructure/AlertService.cs @@ -112,16 +112,20 @@ public sealed class AlertService : IAlertService // Check if all rules match (AND logic) var allRulesMatch = condition.Rules.All(rule => CheckRule(rule, telemetry)); - if (allRulesMatch && condition.Rules.Any()) + if (allRulesMatch && condition.Rules.Count != 0) { // All rules passed, send alert if cooldown period has passed // await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken); - string? message = await GetAlertMessage(condition, device, device.User, telemetry, cancellationToken); - if(message != null && message.Length > 0) - messages.Add(message); + AlertNotification notif = await GetAlertMessage(condition, device, device.User, telemetry, cancellationToken); + if(notif != null && !string.IsNullOrEmpty(notif.Message)) + { + messages.Add(notif.Message); + await dbContext.AlertNotifications.AddAsync(notif, cancellationToken); + } // return message; } } + await dbContext.SaveChangesAsync(cancellationToken); if(telemetry.OldPower == 0 && telemetry.Power == 1) { @@ -163,7 +167,7 @@ public sealed class AlertService : IAlertService }; } - private async Task GetAlertMessage( + private async Task GetAlertMessage( Domain.AlertCondition condition, Domain.Device device, User user, @@ -204,12 +208,12 @@ public sealed class AlertService : IAlertService //MessageOutboxIds = messageOutboxIds, //ErrorMessage = errorMessage, SentAt = sentAt, - IsSent = true + //IsSent = true }; - dbContext.AlertNotifications.Add(notification); + // dbContext.AlertNotifications.Add(notification); - return message; + return notification; } private async Task SendAlertForConditionAsync( diff --git a/src/GreenHome.Infrastructure/GreenHomeDbContext.cs b/src/GreenHome.Infrastructure/GreenHomeDbContext.cs index 3e22b8c..8916f1b 100644 --- a/src/GreenHome.Infrastructure/GreenHomeDbContext.cs +++ b/src/GreenHome.Infrastructure/GreenHomeDbContext.cs @@ -75,6 +75,7 @@ public sealed class GreenHomeDbContext : DbContext 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) diff --git a/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.Designer.cs b/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.Designer.cs new file mode 100644 index 0000000..c3a83c6 --- /dev/null +++ b/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.Designer.cs @@ -0,0 +1,1251 @@ +// +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("20260428054608_AddRelayCountToDeviceSettings")] + partial class AddRelayCountToDeviceSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("Temperature") + .HasColumnType("float"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AIQueries", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CallCooldownMinutes") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SmsCooldownMinutes") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("AlertConditions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("AlertType") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProcessingTimeMs") + .HasColumnType("bigint"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("AlertType"); + + b.HasIndex("Status"); + + b.HasIndex("DeviceId", "SentAt"); + + b.HasIndex("UserId", "SentAt"); + + b.ToTable("AlertLogs", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsSent") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("MessageOutboxIds") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt"); + + b.ToTable("AlertNotifications", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AlertConditionId") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AlertConditionId"); + + b.ToTable("AlertRules", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("DeviceId", "IsActive"); + + b.ToTable("Checklists", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CompletedByUserId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("CompletedByUserId"); + + b.HasIndex("ChecklistId", "PersianDate"); + + b.ToTable("ChecklistCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsRequired") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId", "Order"); + + b.ToTable("ChecklistItems", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChecklistCompletionId") + .HasColumnType("int"); + + b.Property("ChecklistItemId") + .HasColumnType("int"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistCompletionId"); + + b.HasIndex("ChecklistItemId"); + + b.ToTable("ChecklistItemCompletions", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Analysis") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompletionTokens") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("PromptTokens") + .HasColumnType("int"); + + b.Property("RecordCount") + .HasColumnType("int"); + + b.Property("ResponseTimeMs") + .HasColumnType("bigint"); + + b.Property("SampledRecordCount") + .HasColumnType("int"); + + b.Property("TotalTokens") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DeviceId", "PersianDate") + .IsUnique(); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("DailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Location") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("NeshanLocation") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Devices", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("DeviceId", "CreatedAt"); + + b.ToTable("DevicePosts", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DevicePostId") + .HasColumnType("int"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DevicePostId"); + + b.ToTable("DevicePostImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AreaSquareMeters") + .HasColumnType("decimal(18,2)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("DeviceId1") + .HasColumnType("int"); + + b.Property("DevicePhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Latitude") + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasColumnType("decimal(9,6)"); + + b.Property("MinimumCallIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(60); + + b.Property("MinimumSmsIntervalMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(15); + + b.Property("ProductType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Province") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RelayCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + + b.Property("SimCardType") + .HasColumnType("int"); + + b.Property("SmsRecievers") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("TokenCode") + .HasColumnType("nvarchar(max)"); + + b.Property("TokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UploadIntervalMin") + .HasColumnType("int"); + + b.Property("VerificationCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("DeviceId1"); + + b.ToTable("DeviceSettings", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.HasKey("DeviceId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("DeviceUsers", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("datetime2"); + + b.Property("UserDailyReportId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserDailyReportId"); + + b.ToTable("ReportImages", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BatteryPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("BatteryVoltage") + .HasColumnType("decimal(18,2)"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("GasPPM") + .HasColumnType("int"); + + b.Property("HumidityPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("Lux") + .HasColumnType("decimal(18,2)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Power") + .HasColumnType("tinyint"); + + b.Property("ServerTimestampUtc") + .HasColumnType("datetime2"); + + b.Property("SoilPercent") + .HasColumnType("decimal(18,2)"); + + b.Property("TemperatureC") + .HasColumnType("decimal(18,2)"); + + b.Property("TimestampUtc") + .HasColumnType("datetime2"); + + b.Property("Voltage") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId", "ServerTimestampUtc"); + + b.HasIndex("DeviceId", "TimestampUtc"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("Telemetry", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Family") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Mobile") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Observations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Operations") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PersianDate") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("PersianDay") + .HasColumnType("int"); + + b.Property("PersianMonth") + .HasColumnType("int"); + + b.Property("PersianYear") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "PersianDate"); + + b.HasIndex("DeviceId", "PersianYear", "PersianMonth"); + + b.ToTable("UserDailyReports", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.VerificationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(4) + .HasColumnType("nvarchar(4)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsUsed") + .HasColumnType("bit"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("UsedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Mobile", "Code", "IsUsed"); + + b.ToTable("VerificationCodes", (string)null); + }); + + modelBuilder.Entity("GreenHome.Domain.AIQuery", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertLog", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertNotification", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany() + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AlertCondition"); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertRule", b => + { + b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition") + .WithMany("Rules") + .HasForeignKey("AlertConditionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AlertCondition"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.HasOne("GreenHome.Domain.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Completions") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "CompletedByUser") + .WithMany() + .HasForeignKey("CompletedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("CompletedByUser"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b => + { + b.HasOne("GreenHome.Domain.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b => + { + b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion") + .WithMany("ItemCompletions") + .HasForeignKey("ChecklistCompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem") + .WithMany() + .HasForeignKey("ChecklistItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ChecklistCompletion"); + + b.Navigation("ChecklistItem"); + }); + + modelBuilder.Entity("GreenHome.Domain.DailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.HasOne("GreenHome.Domain.User", "AuthorUser") + .WithMany() + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AuthorUser"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b => + { + b.HasOne("GreenHome.Domain.DevicePost", "DevicePost") + .WithMany("Images") + .HasForeignKey("DevicePostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DevicePost"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.Device", null) + .WithMany("DeviceSettings") + .HasForeignKey("DeviceId1"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceUser", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany("DeviceUsers") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany("DeviceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => + { + b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport") + .WithMany("Images") + .HasForeignKey("UserDailyReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserDailyReport"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GreenHome.Domain.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GreenHome.Domain.AlertCondition", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.Checklist", b => + { + b.Navigation("Completions"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b => + { + b.Navigation("ItemCompletions"); + }); + + modelBuilder.Entity("GreenHome.Domain.Device", b => + { + b.Navigation("DeviceSettings"); + + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.DevicePost", b => + { + b.Navigation("Images"); + }); + + modelBuilder.Entity("GreenHome.Domain.User", b => + { + b.Navigation("DeviceUsers"); + }); + + modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b => + { + b.Navigation("Images"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.cs b/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.cs new file mode 100644 index 0000000..1a364a7 --- /dev/null +++ b/src/GreenHome.Infrastructure/Migrations/20260428054608_AddRelayCountToDeviceSettings.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GreenHome.Infrastructure.Migrations +{ + /// + public partial class AddRelayCountToDeviceSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RelayCount", + table: "DeviceSettings", + type: "int", + nullable: false, + defaultValue: 4); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RelayCount", + table: "DeviceSettings"); + } + } +} diff --git a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs index 49af6f7..9e1dbb6 100644 --- a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs +++ b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs @@ -623,6 +623,11 @@ namespace GreenHome.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("RelayCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + b.Property("SimCardType") .HasColumnType("int");