diff --git a/src/GreenHome.Api/Controllers/TelemetryController.cs b/src/GreenHome.Api/Controllers/TelemetryController.cs
index e354c90..4672181 100644
--- a/src/GreenHome.Api/Controllers/TelemetryController.cs
+++ b/src/GreenHome.Api/Controllers/TelemetryController.cs
@@ -77,24 +77,46 @@ public class TelemetryController : ControllerBase
///
/// Encode relay conditions into a compact string format for STM32 parsing.
- /// Format: E{enabled}T{timeType}R{relay}S{sensor}O{op}V{value1}[,{value2}]
- /// Multiple conditions separated by |, multiple rules within a condition separated by &
+ /// Format (relays separated by |):
+ /// E{enabled}R{relayIndex}{groups}
+ /// Group (OR between groups):
+ /// G{action} where action = 0 (turn on) | 1 (turn off)
+ /// followed by its conditions joined by & (AND inside a group):
+ /// Sensor condition: S{sensor}O{op}V{value1}[,{value2}]
+ /// Time condition: T{start}-{end} (minutes from midnight)
+ /// Time ranges with start > end cross midnight (e.g. T1200-360 = 20:00..06:00);
+ /// the device must treat that as (now >= start || now < end).
///
private static string EncodeRelayConditions(IReadOnlyList conditions)
{
return string.Join("|", conditions.Select(c =>
{
- var rules = string.Join("&", c.Rules.Select(r =>
- {
- var rule = $"S{r.Sensor}O{r.Op}V{r.V1}";
- if (r.V2.HasValue)
- rule += $",{r.V2.Value}";
- return rule;
- }));
- return $"E{c.Enabled}T{c.TimeType}R{c.Relay}{rules}";
+ var groups = new List();
+ groups.AddRange(c.On.Select(g => EncodeGroup(g, 0)));
+ groups.AddRange(c.Off.Select(g => EncodeGroup(g, 1)));
+ return $"E{c.Enabled}R{c.RelayIndex}{string.Concat(groups)}";
}));
}
+ private static string EncodeGroup(RelayGroupDto group, int action)
+ {
+ return $"G{action}{string.Join("&", group.Conditions.Select(EncodeCondition))}";
+ }
+
+ private static string EncodeCondition(RelayRuleDto condition)
+ {
+ if (condition.Kind == 1)
+ {
+ return $"T{condition.Start ?? 0}-{condition.End ?? 0}";
+ }
+
+ var encoded = $"S{condition.Sensor}O{condition.Op}V{condition.V1}";
+ if (condition.V2.HasValue)
+ encoded += $",{condition.V2.Value}";
+
+ return encoded;
+ }
+
///
/// Collect relay states from query params r1, r2, ... (missing relay = off/0).
///
diff --git a/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs b/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs
index 27841e3..67749d2 100644
--- a/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs
+++ b/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs
@@ -1,43 +1,54 @@
namespace GreenHome.Application;
///
-/// DTO for relay alert conditions (notificationType > 0)
+/// DTO for relay conditions sent to the device firmware.
///
public sealed class RelayConditionDto
{
public int Enabled { get; set; }
- public int TimeType { get; set; }
- ///
- /// شناسه رله برای سازگاری با فریمور قبلی (مثلاً R1)
- ///
+ /// شناسه رله برای سازگاری با فریمور قبلی (مثلاً R1)
public string Relay { get; set; } = string.Empty;
- ///
- /// نام نمایشی رله (فن، هیتر و ...)
- ///
+ /// شماره رله (۱ تا RelayCount)
+ public int RelayIndex { get; set; }
+
+ /// نام نمایشی رله (فن، هیتر و ...)
public string Name { get; set; } = string.Empty;
- public List Rules { get; set; } = new();
+ /// گروههای روشن شدن (OR بین گروهها)
+ public List On { get; set; } = new();
+
+ /// گروههای خاموش شدن (OR بین گروهها)
+ public List Off { get; set; } = new();
}
///
-/// DTO for relay condition rules
+/// یک گروه شرط (AND داخل گروه)
+///
+public sealed class RelayGroupDto
+{
+ /// برچسب اختیاری گروه برای لاگ/اعلان
+ public string? Label { get; set; }
+
+ public List Conditions { get; set; } = new();
+}
+
+///
+/// یک شرط واحد: سنسوری (Kind=0) یا زمانی (Kind=1)
///
public sealed class RelayRuleDto
{
- public string Sensor { get; set; } = string.Empty;
- public string Op { get; set; } = string.Empty;
- public decimal V1 { get; set; }
+ /// نوع شرط: 0 = سنسوری، 1 = زمانی
+ public int Kind { get; set; }
+
+ // سنسوری
+ public int? Sensor { get; set; }
+ public int? Op { get; set; }
+ public decimal? V1 { get; set; }
public decimal? V2 { get; set; }
- ///
- /// اقدام: 0 = روشن، 1 = خاموش
- ///
- public int Action { get; set; }
-
- ///
- /// زمان اعمال: 0 = روز، 1 = شب، 2 = همیشه
- ///
- public int TimeType { get; set; }
+ // زمانی (دقیقه از نیمهشب)
+ public int? Start { get; set; }
+ public int? End { get; set; }
}
diff --git a/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs b/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs
index 70f9ac0..594ade9 100644
--- a/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs
+++ b/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs
@@ -13,20 +13,51 @@ public sealed class DeviceRelayDto
public string Name { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public Domain.RelayKind RelayKind { get; set; } = Domain.RelayKind.Generic;
- public List Rules { get; set; } = new();
+
+ ///
+ /// گروههای شرط رله. گروههای همعمل (روشن یا خاموش) با OR ترکیب میشوند
+ /// و شرایط داخل هر گروه با AND.
+ ///
+ public List RuleGroups { get; set; } = new();
}
///
-/// قانون روشن/خاموش شدن رله
+/// یک گروه شرط برای روشن یا خاموش شدن رله
///
-public sealed class DeviceRelayRuleDto
+public sealed class DeviceRelayRuleGroupDto
{
public int Id { get; set; }
- public Domain.SensorType SensorType { get; set; }
- public Domain.ComparisonType ComparisonType { get; set; }
- public decimal Value1 { get; set; }
- public decimal? Value2 { get; set; }
+
+ /// اقدام: روشن (On) یا خاموش (Off)
public Domain.RelayAction Action { get; set; } = Domain.RelayAction.On;
- public Domain.AlertTimeType TimeType { get; set; } = Domain.AlertTimeType.Always;
+
+ /// برچسب اختیاری گروه برای لاگ/اعلان
+ public string? Label { get; set; }
+
+ public int Order { get; set; }
+
+ public List Conditions { get; set; } = new();
+}
+
+///
+/// یک شرط واحد داخل گروه (سنسوری یا زمانی)
+///
+public sealed class DeviceRelayRuleConditionDto
+{
+ public int Id { get; set; }
+
+ /// نوع شرط: سنسوری (0) یا زمانی (1)
+ public Domain.RelayConditionType ConditionType { get; set; } = Domain.RelayConditionType.Sensor;
+
+ // ---- شرط سنسوری ----
+ public Domain.SensorType? SensorType { get; set; }
+ public Domain.ComparisonType? ComparisonType { get; set; }
+ public decimal? Value1 { get; set; }
+ public decimal? Value2 { get; set; }
+
+ // ---- شرط زمانی (دقیقه از نیمهشب) ----
+ public int? StartMinutes { get; set; }
+ public int? EndMinutes { get; set; }
+
public int Order { get; set; }
}
diff --git a/src/GreenHome.Application/MappingProfile.cs b/src/GreenHome.Application/MappingProfile.cs
index c292cac..8c2fe01 100644
--- a/src/GreenHome.Application/MappingProfile.cs
+++ b/src/GreenHome.Application/MappingProfile.cs
@@ -25,12 +25,16 @@ public sealed class MappingProfile : Profile
CreateMap()
.ReverseMap()
.ForMember(dest => dest.DeviceSettings, opt => opt.Ignore())
- .ForMember(dest => dest.Rules, opt => opt.Ignore());
+ .ForMember(dest => dest.RuleGroups, opt => opt.Ignore());
- CreateMap()
+ CreateMap()
.ReverseMap()
.ForMember(dest => dest.DeviceRelay, opt => opt.Ignore());
+ CreateMap()
+ .ReverseMap()
+ .ForMember(dest => dest.RelayRuleGroup, opt => opt.Ignore());
+
CreateMap()
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
.ReverseMap()
diff --git a/src/GreenHome.Domain/DeviceRelay.cs b/src/GreenHome.Domain/DeviceRelay.cs
index c213bcb..9f0c2d3 100644
--- a/src/GreenHome.Domain/DeviceRelay.cs
+++ b/src/GreenHome.Domain/DeviceRelay.cs
@@ -63,37 +63,74 @@ public sealed class DeviceRelay
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
- /// قوانین روشن/خاموش شدن رله
- public ICollection Rules { get; set; } = new List();
+ /// گروههای شرط روشن/خاموش شدن رله (گروهها با OR ترکیب میشوند)
+ public ICollection RuleGroups { get; set; } = new List();
}
///
-/// یک قانون برای روشن یا خاموش کردن رله بر اساس سنسورها
+/// یک گروه شرط برای روشن یا خاموش کردن رله.
+/// داخل هر گروه شرایط با AND ترکیب میشوند؛ گروههای همعمل با OR.
///
-public sealed class RelayRule
+public sealed class RelayRuleGroup
{
public int Id { get; set; }
public int DeviceRelayId { get; set; }
public DeviceRelay DeviceRelay { get; set; } = null!;
+ /// اقدام: روشن یا خاموش کردن رله
+ public RelayAction Action { get; set; } = RelayAction.On;
+
+ /// برچسب اختیاری گروه (مثلاً «ایمنی»، «خنکسازی») برای استفاده در لاگ/اعلان
+ public string? Label { get; set; }
+
+ /// ترتیب گروهها
+ public int Order { get; set; }
+
+ /// شرایط داخل این گروه (AND)
+ public ICollection Conditions { get; set; } = new List();
+}
+
+///
+/// نوع یک شرط رله: سنسوری یا زمانی
+///
+public enum RelayConditionType
+{
+ Sensor = 0,
+ Time = 1
+}
+
+///
+/// یک شرط واحد داخل گروه (سنسوری یا بازه زمانی)
+///
+public sealed class RelayRuleCondition
+{
+ public int Id { get; set; }
+ public int RelayRuleGroupId { get; set; }
+ public RelayRuleGroup RelayRuleGroup { get; set; } = null!;
+
+ /// نوع شرط: سنسوری یا زمانی
+ public RelayConditionType ConditionType { get; set; } = RelayConditionType.Sensor;
+
+ // ---- شرط سنسوری ----
/// نوع سنسور: دما، رطوبت، خاک، گاز، نور
- public SensorType SensorType { get; set; }
+ public SensorType? SensorType { get; set; }
/// نوع مقایسه: بزرگتر، کوچکتر، بین، خارج از محدوده
- public ComparisonType ComparisonType { get; set; }
+ public ComparisonType? ComparisonType { get; set; }
/// مقدار عددی اول
- public decimal Value1 { get; set; }
+ public decimal? Value1 { get; set; }
/// مقدار عددی دوم (برای بین و خارج از محدوده)
public decimal? Value2 { get; set; }
- /// اقدام: روشن یا خاموش کردن رله
- public RelayAction Action { get; set; } = RelayAction.On;
+ // ---- شرط زمانی ----
+ /// شروع بازه به دقیقه از نیمهشب (۰ تا ۱۴۳۹)
+ public int? StartMinutes { get; set; }
- /// زمان اعمال: روز، شب، همیشه
- public AlertTimeType TimeType { get; set; } = AlertTimeType.Always;
+ /// پایان بازه به دقیقه از نیمهشب (۰ تا ۱۴۳۹) — اگر کوچکتر از شروع باشد یعنی از نیمهشب عبور میکند
+ public int? EndMinutes { get; set; }
- /// ترتیب قوانین
+ /// ترتیب شرایط داخل گروه
public int Order { get; set; }
}
diff --git a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs
index d40e435..f70c3af 100644
--- a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs
+++ b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs
@@ -12,7 +12,8 @@ public sealed class GreenHomeDbContext : DbContext
public DbSet AlertConditions => Set();
public DbSet AlertRules => Set();
public DbSet DeviceRelays => Set();
- public DbSet RelayRules => Set();
+ public DbSet RelayRuleGroups => Set();
+ public DbSet RelayRuleConditions => Set();
public DbSet Users => Set();
public DbSet VerificationCodes => Set();
public DbSet DeviceUsers => Set();
@@ -101,24 +102,37 @@ public sealed class GreenHomeDbContext : DbContext
b.HasIndex(x => new { x.DeviceSettingsId, x.RelayIndex }).IsUnique();
});
- modelBuilder.Entity(b =>
+ modelBuilder.Entity(b =>
{
- b.ToTable("RelayRules");
+ b.ToTable("RelayRuleGroups");
b.HasKey(x => x.Id);
- b.Property(x => x.SensorType).IsRequired().HasConversion();
- b.Property(x => x.ComparisonType).IsRequired().HasConversion();
b.Property(x => x.Action).IsRequired().HasConversion();
- b.Property(x => x.TimeType).IsRequired().HasConversion();
- b.Property(x => x.Value1).IsRequired().HasColumnType("decimal(18,2)");
- b.Property(x => x.Value2).HasColumnType("decimal(18,2)");
+ b.Property(x => x.Label).HasMaxLength(200);
b.Property(x => x.Order).IsRequired();
b.HasOne(x => x.DeviceRelay)
- .WithMany(r => r.Rules)
+ .WithMany(r => r.RuleGroups)
.HasForeignKey(x => x.DeviceRelayId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DeviceRelayId);
});
+ modelBuilder.Entity(b =>
+ {
+ b.ToTable("RelayRuleConditions");
+ b.HasKey(x => x.Id);
+ b.Property(x => x.ConditionType).IsRequired().HasConversion();
+ b.Property(x => x.SensorType).HasConversion();
+ b.Property(x => x.ComparisonType).HasConversion();
+ b.Property(x => x.Value1).HasColumnType("decimal(18,2)");
+ b.Property(x => x.Value2).HasColumnType("decimal(18,2)");
+ b.Property(x => x.Order).IsRequired();
+ b.HasOne(x => x.RelayRuleGroup)
+ .WithMany(g => g.Conditions)
+ .HasForeignKey(x => x.RelayRuleGroupId)
+ .OnDelete(DeleteBehavior.Cascade);
+ b.HasIndex(x => x.RelayRuleGroupId);
+ });
+
modelBuilder.Entity(b =>
{
b.ToTable("AlertConditions");
diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260911195203_RelayRuleGroupsAndConditions.Designer.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260911195203_RelayRuleGroupsAndConditions.Designer.cs
new file mode 100644
index 0000000..b1e8537
--- /dev/null
+++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260911195203_RelayRuleGroupsAndConditions.Designer.cs
@@ -0,0 +1,1498 @@
+//
+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.Migrations
+{
+ [DbContext(typeof(GreenHomeDbContext))]
+ [Migration("20260911195203_RelayRuleGroupsAndConditions")]
+ partial class RelayRuleGroupsAndConditions
+ {
+ ///
+ 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("PostType")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("VoiceDurationSeconds")
+ .HasColumnType("int");
+
+ b.Property("VoiceFilePath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ 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.DeviceRelay", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceSettingsId")
+ .HasColumnType("int");
+
+ b.Property("IsEnabled")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RelayIndex")
+ .HasColumnType("int");
+
+ b.Property("RelayKind")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceSettingsId", "RelayIndex")
+ .IsUnique();
+
+ b.ToTable("DeviceRelays", (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("AvatarPath")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Family")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("ReceiveAlerts")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.HasKey("DeviceId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("DeviceUsers", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CurrentPower")
+ .HasColumnType("tinyint");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("EventType")
+ .HasColumnType("int");
+
+ b.Property("PersianDate")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("PersianMonth")
+ .HasColumnType("int");
+
+ b.Property("PersianYear")
+ .HasColumnType("int");
+
+ b.Property("PreviousPower")
+ .HasColumnType("tinyint");
+
+ b.Property("ServerTimestampUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("TimestampUtc")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventType");
+
+ b.HasIndex("DeviceId", "TimestampUtc");
+
+ b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
+
+ b.ToTable("PowerOutageLogs", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.RelayRuleCondition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ComparisonType")
+ .HasColumnType("int");
+
+ b.Property("ConditionType")
+ .HasColumnType("int");
+
+ b.Property("EndMinutes")
+ .HasColumnType("int");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.Property("RelayRuleGroupId")
+ .HasColumnType("int");
+
+ b.Property("SensorType")
+ .HasColumnType("int");
+
+ b.Property("StartMinutes")
+ .HasColumnType("int");
+
+ b.Property("Value1")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Value2")
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RelayRuleGroupId");
+
+ b.ToTable("RelayRuleConditions", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.RelayRuleGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Action")
+ .HasColumnType("int");
+
+ b.Property("DeviceRelayId")
+ .HasColumnType("int");
+
+ b.Property("Label")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceRelayId");
+
+ b.ToTable("RelayRuleGroups", (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