diff --git a/src/GreenHome.Api/Controllers/TelemetryController.cs b/src/GreenHome.Api/Controllers/TelemetryController.cs index 5a6ba73..112a334 100644 --- a/src/GreenHome.Api/Controllers/TelemetryController.cs +++ b/src/GreenHome.Api/Controllers/TelemetryController.cs @@ -20,9 +20,9 @@ public class TelemetryController : ControllerBase } [HttpGet] - public async Task>> List([FromQuery] int? deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default) + public async Task>> List([FromQuery] int? deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] DateTime? sinceUtc, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default) { - var filter = new TelemetryFilter { DeviceId = deviceId, StartDateUtc = startUtc, EndDateUtc = endUtc, Page = page, PageSize = pageSize }; + var filter = new TelemetryFilter { DeviceId = deviceId, StartDateUtc = startUtc, EndDateUtc = endUtc, SinceUtc = sinceUtc, Page = page, PageSize = pageSize }; var result = await telemetryService.ListAsync(filter, cancellationToken); return Ok(result); } @@ -135,4 +135,11 @@ public class TelemetryController : ControllerBase var result = await telemetryService.GetActiveMonthsAsync(deviceId, year, cancellationToken); return Ok(result); } + + [HttpGet("aggregate")] + public async Task>> Aggregate([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] int bucketMinutes = 15, CancellationToken cancellationToken = default) + { + var result = await telemetryService.GetAggregatedAsync(deviceId, startUtc, endUtc, bucketMinutes, cancellationToken); + return Ok(result); + } } diff --git a/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs b/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs index 73d3f0f..27841e3 100644 --- a/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs +++ b/src/GreenHome.Application/Dtos/Alert/RelayConditionDto.cs @@ -7,7 +7,17 @@ public sealed class RelayConditionDto { public int Enabled { get; set; } public int TimeType { get; set; } + + /// + /// شناسه رله برای سازگاری با فریم‌ور قبلی (مثلاً R1) + /// public string Relay { get; set; } = string.Empty; + + /// + /// نام نمایشی رله (فن، هیتر و ...) + /// + public string Name { get; set; } = string.Empty; + public List Rules { get; set; } = new(); } @@ -20,4 +30,14 @@ public sealed class RelayRuleDto public string Op { get; set; } = string.Empty; 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; } } diff --git a/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs b/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs new file mode 100644 index 0000000..c532136 --- /dev/null +++ b/src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs @@ -0,0 +1,35 @@ +using Domain = GreenHome.Domain; + +namespace GreenHome.Application; + +/// +/// تنظیمات یک رله دستگاه +/// +public sealed class DeviceRelayDto +{ + public int Id { get; set; } + public int DeviceSettingsId { get; set; } + public int RelayIndex { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsEnabled { get; set; } = true; + public Domain.RelayKind RelayKind { get; set; } = Domain.RelayKind.Generic; + public Domain.RelayMode Mode { get; set; } = Domain.RelayMode.Auto; + public int MinOnMinutes { get; set; } = 0; + public int MinOffMinutes { get; set; } = 0; + public List Rules { get; set; } = new(); +} + +/// +/// قانون روشن/خاموش شدن رله +/// +public sealed class DeviceRelayRuleDto +{ + 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; } + public Domain.RelayAction Action { get; set; } = Domain.RelayAction.On; + public Domain.AlertTimeType TimeType { get; set; } = Domain.AlertTimeType.Always; + public int Order { get; set; } +} diff --git a/src/GreenHome.Application/Dtos/Device/DeviceSettingsDto.cs b/src/GreenHome.Application/Dtos/Device/DeviceSettingsDto.cs index e2d5483..1d8651a 100644 --- a/src/GreenHome.Application/Dtos/Device/DeviceSettingsDto.cs +++ b/src/GreenHome.Application/Dtos/Device/DeviceSettingsDto.cs @@ -25,6 +25,8 @@ public sealed class DeviceSettingsDto public string? VerificationCode { get; set; } public DateTime? TokenExpiresAt { get; set; } + public List Relays { get; set; } = new(); + public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } diff --git a/src/GreenHome.Application/Dtos/Telemetry/TelemetryBucketDto.cs b/src/GreenHome.Application/Dtos/Telemetry/TelemetryBucketDto.cs new file mode 100644 index 0000000..01695c6 --- /dev/null +++ b/src/GreenHome.Application/Dtos/Telemetry/TelemetryBucketDto.cs @@ -0,0 +1,22 @@ +namespace GreenHome.Application; + +public sealed class TelemetryBucketDto +{ + public DateTime BucketStart { get; set; } + public decimal MinTemperatureC { get; set; } + public decimal MaxTemperatureC { get; set; } + public decimal AvgTemperatureC { get; set; } + public decimal MinHumidityPercent { get; set; } + public decimal MaxHumidityPercent { get; set; } + public decimal AvgHumidityPercent { get; set; } + public decimal MinSoilPercent { get; set; } + public decimal MaxSoilPercent { get; set; } + public decimal AvgSoilPercent { get; set; } + public int MinGasPPM { get; set; } + public int MaxGasPPM { get; set; } + public decimal AvgGasPPM { get; set; } + public decimal MinLux { get; set; } + public decimal MaxLux { get; set; } + public decimal AvgLux { get; set; } + public int Count { get; set; } +} diff --git a/src/GreenHome.Application/Dtos/Telemetry/TelemetryFilter.cs b/src/GreenHome.Application/Dtos/Telemetry/TelemetryFilter.cs index 0da8e27..e0a49bc 100644 --- a/src/GreenHome.Application/Dtos/Telemetry/TelemetryFilter.cs +++ b/src/GreenHome.Application/Dtos/Telemetry/TelemetryFilter.cs @@ -5,6 +5,7 @@ public sealed class TelemetryFilter public int? DeviceId { get; set; } public DateTime? StartDateUtc { get; set; } public DateTime? EndDateUtc { get; set; } + public DateTime? SinceUtc { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } diff --git a/src/GreenHome.Application/Dtos/Telemetry/TelemetryMinMax.cs b/src/GreenHome.Application/Dtos/Telemetry/TelemetryMinMax.cs index 4d9707d..ac64f65 100644 --- a/src/GreenHome.Application/Dtos/Telemetry/TelemetryMinMax.cs +++ b/src/GreenHome.Application/Dtos/Telemetry/TelemetryMinMax.cs @@ -4,12 +4,18 @@ public sealed class TelemetryMinMax { public decimal MinTemperatureC { get; set; } public decimal MaxTemperatureC { get; set; } + public decimal AvgTemperatureC { get; set; } public decimal MinHumidityPercent { get; set; } public decimal MaxHumidityPercent { get; set; } + public decimal AvgHumidityPercent { get; set; } public decimal MinSoilPercent { get; set; } public decimal MaxSoilPercent { get; set; } + public decimal AvgSoilPercent { get; set; } public int MinGasPPM { get; set; } public int MaxGasPPM { get; set; } + public decimal AvgGasPPM { get; set; } public decimal MinLux { get; set; } public decimal MaxLux { get; set; } + public decimal AvgLux { get; set; } + public int Count { get; set; } } diff --git a/src/GreenHome.Application/Interfaces/ITelemetryService.cs b/src/GreenHome.Application/Interfaces/ITelemetryService.cs index 1e16150..d4fbfab 100644 --- a/src/GreenHome.Application/Interfaces/ITelemetryService.cs +++ b/src/GreenHome.Application/Interfaces/ITelemetryService.cs @@ -7,4 +7,5 @@ public interface ITelemetryService Task GetMinMaxAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, CancellationToken cancellationToken); Task> GetMonthDaysAsync(int deviceId, int persianYear, int persianMonth, CancellationToken cancellationToken); Task> GetActiveMonthsAsync(int deviceId, int persianYear, CancellationToken cancellationToken); + Task> GetAggregatedAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, int bucketMinutes, CancellationToken cancellationToken); } diff --git a/src/GreenHome.Application/MappingProfile.cs b/src/GreenHome.Application/MappingProfile.cs index f4aabbf..e87ed71 100644 --- a/src/GreenHome.Application/MappingProfile.cs +++ b/src/GreenHome.Application/MappingProfile.cs @@ -19,7 +19,17 @@ public sealed class MappingProfile : Profile CreateMap() .ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName)) .ReverseMap() - .ForMember(dest => dest.Device, opt => opt.Ignore()); + .ForMember(dest => dest.Device, opt => opt.Ignore()) + .ForMember(dest => dest.Relays, opt => opt.Ignore()); + + CreateMap() + .ReverseMap() + .ForMember(dest => dest.DeviceSettings, opt => opt.Ignore()) + .ForMember(dest => dest.Rules, opt => opt.Ignore()); + + CreateMap() + .ReverseMap() + .ForMember(dest => dest.DeviceRelay, opt => opt.Ignore()); CreateMap() .ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName)) diff --git a/src/GreenHome.Domain/DeviceRelay.cs b/src/GreenHome.Domain/DeviceRelay.cs new file mode 100644 index 0000000..601ea27 --- /dev/null +++ b/src/GreenHome.Domain/DeviceRelay.cs @@ -0,0 +1,120 @@ +namespace GreenHome.Domain; + +/// +/// نوع مصرف‌کننده متصل به رله +/// +public enum RelayKind +{ + /// سایر / نامشخص + Generic = 0, + + /// فن + Fan = 1, + + /// هیتر / بخاری + Heater = 2, + + /// پمپ آب + WaterPump = 3, + + /// مه‌پاش / رطوبت‌ساز + Humidifier = 4, + + /// روشنایی + Light = 5, + + /// شیر برقی + Valve = 6 +} + +/// +/// حالت عملکرد رله +/// +public enum RelayMode +{ + /// خودکار (بر اساس قوانین) + Auto = 0, + + /// دستی (قوانین خودکار نادیده گرفته می‌شوند) + Manual = 1 +} + +/// +/// نوع اقدام یک قانون رله +/// +public enum RelayAction +{ + /// روشن کردن + On = 0, + + /// خاموش کردن + Off = 1 +} + +/// +/// رله دستگاه به همراه تنظیمات آن +/// +public sealed class DeviceRelay +{ + public int Id { get; set; } + public int DeviceSettingsId { get; set; } + public DeviceSettings DeviceSettings { get; set; } = null!; + + /// شماره رله (۱ تا RelayCount) + public int RelayIndex { get; set; } + + /// نام نمایشی رله (مثلاً فن، هیتر) + public string Name { get; set; } = string.Empty; + + /// آیا این رله فعال است؟ + public bool IsEnabled { get; set; } = true; + + /// نوع مصرف‌کننده متصل به رله + public RelayKind RelayKind { get; set; } = RelayKind.Generic; + + /// حالت عملکرد: خودکار یا دستی + public RelayMode Mode { get; set; } = RelayMode.Auto; + + /// حداقل زمان روشن ماندن (دقیقه) - برای جلوگیری از قطع و وصل مکرر + public int MinOnMinutes { get; set; } = 0; + + /// حداقل زمان خاموش ماندن (دقیقه) - برای محافظت از تجهیزات + public int MinOffMinutes { get; set; } = 0; + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + /// قوانین روشن/خاموش شدن رله + public ICollection Rules { get; set; } = new List(); +} + +/// +/// یک قانون برای روشن یا خاموش کردن رله بر اساس سنسورها +/// +public sealed class RelayRule +{ + public int Id { get; set; } + public int DeviceRelayId { get; set; } + public DeviceRelay DeviceRelay { get; set; } = null!; + + /// نوع سنسور: دما، رطوبت، خاک، گاز، نور + public SensorType SensorType { get; set; } + + /// نوع مقایسه: بزرگتر، کوچکتر، بین، خارج از محدوده + public ComparisonType ComparisonType { get; set; } + + /// مقدار عددی اول + public decimal Value1 { get; set; } + + /// مقدار عددی دوم (برای بین و خارج از محدوده) + public decimal? Value2 { get; set; } + + /// اقدام: روشن یا خاموش کردن رله + public RelayAction Action { get; set; } = RelayAction.On; + + /// زمان اعمال: روز، شب، همیشه + public AlertTimeType TimeType { get; set; } = AlertTimeType.Always; + + /// ترتیب قوانین + public int Order { get; set; } +} diff --git a/src/GreenHome.Domain/DeviceSettings.cs b/src/GreenHome.Domain/DeviceSettings.cs index 8bb7eae..68d8f94 100644 --- a/src/GreenHome.Domain/DeviceSettings.cs +++ b/src/GreenHome.Domain/DeviceSettings.cs @@ -109,4 +109,9 @@ public sealed class DeviceSettings public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } + + /// + /// تنظیمات هر رله دستگاه + /// + public ICollection Relays { get; set; } = new List(); } \ No newline at end of file diff --git a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs index 230e55f..f3d1424 100644 --- a/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs +++ b/src/GreenHome.Infrastructure/Data/GreenHomeDbContext.cs @@ -11,6 +11,8 @@ public sealed class GreenHomeDbContext : DbContext public DbSet DeviceSettings => Set(); public DbSet AlertConditions => Set(); public DbSet AlertRules => Set(); + public DbSet DeviceRelays => Set(); + public DbSet RelayRules => Set(); public DbSet Users => Set(); public DbSet VerificationCodes => Set(); public DbSet DeviceUsers => Set(); @@ -82,6 +84,39 @@ public sealed class GreenHomeDbContext : DbContext .HasForeignKey(x => x.DeviceId) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => x.DeviceId).IsUnique(); + b.HasMany(x => x.Relays) + .WithOne(r => r.DeviceSettings) + .HasForeignKey(r => r.DeviceSettingsId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(b => + { + b.ToTable("DeviceRelays"); + b.HasKey(x => x.Id); + b.Property(x => x.RelayIndex).IsRequired(); + b.Property(x => x.Name).HasMaxLength(100); + b.Property(x => x.RelayKind).IsRequired().HasConversion(); + b.Property(x => x.Mode).IsRequired().HasConversion(); + b.HasIndex(x => new { x.DeviceSettingsId, x.RelayIndex }).IsUnique(); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RelayRules"); + 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.Order).IsRequired(); + b.HasOne(x => x.DeviceRelay) + .WithMany(r => r.Rules) + .HasForeignKey(x => x.DeviceRelayId) + .OnDelete(DeleteBehavior.Cascade); + b.HasIndex(x => x.DeviceRelayId); }); modelBuilder.Entity(b => diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.Designer.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.Designer.cs new file mode 100644 index 0000000..6031bfc --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.Designer.cs @@ -0,0 +1,1429 @@ +// +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("20260903014200_AddDeviceRelays")] + partial class AddDeviceRelays + { + /// + 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.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("MinOffMinutes") + .HasColumnType("int"); + + b.Property("MinOnMinutes") + .HasColumnType("int"); + + b.Property("Mode") + .HasColumnType("int"); + + 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("ReceiveAlerts") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + 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.RelayRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("DeviceRelayId") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceRelayId"); + + b.ToTable("RelayRules", (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.DeviceRelay", b => + { + b.HasOne("GreenHome.Domain.DeviceSettings", "DeviceSettings") + .WithMany("Relays") + .HasForeignKey("DeviceSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceSettings"); + }); + + 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.PowerOutageLog", b => + { + b.HasOne("GreenHome.Domain.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.HasOne("GreenHome.Domain.DeviceRelay", "DeviceRelay") + .WithMany("Rules") + .HasForeignKey("DeviceRelayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceRelay"); + }); + + 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.DeviceRelay", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Navigation("Relays"); + }); + + 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/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.cs new file mode 100644 index 0000000..5cfe31f --- /dev/null +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GreenHome.Infrastructure.Data.Migrations.Migrations +{ + /// + public partial class AddDeviceRelays : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DeviceRelays", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceSettingsId = table.Column(type: "int", nullable: false), + RelayIndex = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + IsEnabled = table.Column(type: "bit", nullable: false), + RelayKind = table.Column(type: "int", nullable: false), + Mode = table.Column(type: "int", nullable: false), + MinOnMinutes = table.Column(type: "int", nullable: false), + MinOffMinutes = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceRelays", x => x.Id); + table.ForeignKey( + name: "FK_DeviceRelays_DeviceSettings_DeviceSettingsId", + column: x => x.DeviceSettingsId, + principalTable: "DeviceSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RelayRules", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + DeviceRelayId = table.Column(type: "int", nullable: false), + SensorType = table.Column(type: "int", nullable: false), + ComparisonType = table.Column(type: "int", nullable: false), + Value1 = table.Column(type: "decimal(18,2)", nullable: false), + Value2 = table.Column(type: "decimal(18,2)", nullable: true), + Action = table.Column(type: "int", nullable: false), + TimeType = table.Column(type: "int", nullable: false), + Order = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RelayRules", x => x.Id); + table.ForeignKey( + name: "FK_RelayRules_DeviceRelays_DeviceRelayId", + column: x => x.DeviceRelayId, + principalTable: "DeviceRelays", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DeviceRelays_DeviceSettingsId_RelayIndex", + table: "DeviceRelays", + columns: new[] { "DeviceSettingsId", "RelayIndex" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RelayRules_DeviceRelayId", + table: "RelayRules", + column: "DeviceRelayId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RelayRules"); + + migrationBuilder.DropTable( + name: "DeviceRelays"); + } + } +} diff --git a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs index ec04636..f109f17 100644 --- a/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs +++ b/src/GreenHome.Infrastructure/Data/Migrations/Migrations/GreenHomeDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using GreenHome.Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -568,6 +568,54 @@ namespace GreenHome.Infrastructure.Data.Migrations 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("MinOffMinutes") + .HasColumnType("int"); + + b.Property("MinOnMinutes") + .HasColumnType("int"); + + b.Property("Mode") + .HasColumnType("int"); + + 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") @@ -729,6 +777,45 @@ namespace GreenHome.Infrastructure.Data.Migrations b.ToTable("PowerOutageLogs", (string)null); }); + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ComparisonType") + .HasColumnType("int"); + + b.Property("DeviceRelayId") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SensorType") + .HasColumnType("int"); + + b.Property("TimeType") + .HasColumnType("int"); + + b.Property("Value1") + .HasColumnType("decimal(18,2)"); + + b.Property("Value2") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceRelayId"); + + b.ToTable("RelayRules", (string)null); + }); + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => { b.Property("Id") @@ -1188,6 +1275,17 @@ namespace GreenHome.Infrastructure.Data.Migrations b.Navigation("DevicePost"); }); + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.HasOne("GreenHome.Domain.DeviceSettings", "DeviceSettings") + .WithMany("Relays") + .HasForeignKey("DeviceSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceSettings"); + }); + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => { b.HasOne("GreenHome.Domain.Device", "Device") @@ -1233,6 +1331,17 @@ namespace GreenHome.Infrastructure.Data.Migrations b.Navigation("Device"); }); + modelBuilder.Entity("GreenHome.Domain.RelayRule", b => + { + b.HasOne("GreenHome.Domain.DeviceRelay", "DeviceRelay") + .WithMany("Rules") + .HasForeignKey("DeviceRelayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceRelay"); + }); + modelBuilder.Entity("GreenHome.Domain.ReportImage", b => { b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport") @@ -1292,6 +1401,16 @@ namespace GreenHome.Infrastructure.Data.Migrations b.Navigation("Images"); }); + modelBuilder.Entity("GreenHome.Domain.DeviceRelay", b => + { + b.Navigation("Rules"); + }); + + modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b => + { + b.Navigation("Relays"); + }); + modelBuilder.Entity("GreenHome.Domain.User", b => { b.Navigation("DeviceUsers"); diff --git a/src/GreenHome.Infrastructure/Services/Alert/AlertConditionService.cs b/src/GreenHome.Infrastructure/Services/Alert/AlertConditionService.cs index 21a11c5..249b2ca 100644 --- a/src/GreenHome.Infrastructure/Services/Alert/AlertConditionService.cs +++ b/src/GreenHome.Infrastructure/Services/Alert/AlertConditionService.cs @@ -123,24 +123,29 @@ public sealed class AlertConditionService : IAlertConditionService public async Task> GetRelayConditionsAsync(int deviceId, CancellationToken cancellationToken) { - var conditions = await _dbContext.AlertConditions + var relays = await _dbContext.DeviceRelays .Include(x => x.Rules) - .Where(x => x.DeviceId == deviceId && (int)x.NotificationType > 0) - .OrderByDescending(x => x.CreatedAt) + .Where(x => x.DeviceSettings.DeviceId == deviceId && x.IsEnabled) + .OrderBy(x => x.RelayIndex) .AsNoTracking() .ToListAsync(cancellationToken); - var result = conditions.Select(c => new RelayConditionDto + var result = relays.Select(relay => 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 + Enabled = relay.IsEnabled ? 1 : 0, + TimeType = relay.Rules.Any() + ? (int)relay.Rules.OrderBy(r => r.Order).First().TimeType + : (int)Domain.AlertTimeType.Always, + Relay = "R" + relay.RelayIndex, + Name = relay.Name, + Rules = relay.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 + V2 = r.Value2, + Action = (int)r.Action, + TimeType = (int)r.TimeType }).ToList() }).ToList(); @@ -152,8 +157,8 @@ public sealed class AlertConditionService : IAlertConditionService 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); + var hasUpdates = await _dbContext.DeviceRelays + .AnyAsync(x => x.DeviceSettings.DeviceId == deviceId && x.UpdatedAt > lastUpdateDate, cancellationToken); if (!hasUpdates) return Array.Empty(); @@ -165,7 +170,7 @@ public sealed class AlertConditionService : IAlertConditionService { if (lastUpdateDate == null) return true; - return await _dbContext.AlertConditions - .AnyAsync(x => x.DeviceId == deviceId && (int)x.NotificationType > 0 && x.UpdatedAt > lastUpdateDate, cancellationToken); + return await _dbContext.DeviceRelays + .AnyAsync(x => x.DeviceSettings.DeviceId == deviceId && x.UpdatedAt > lastUpdateDate, cancellationToken); } } diff --git a/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs b/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs index 30acec7..ce2f10d 100644 --- a/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs +++ b/src/GreenHome.Infrastructure/Services/Device/DeviceSettingsService.cs @@ -20,10 +20,17 @@ public sealed class DeviceSettingsService : IDeviceSettingsService { var settings = await _dbContext.DeviceSettings .Include(x => x.Device) + .Include(x => x.Relays) + .ThenInclude(r => r.Rules) .AsNoTracking() .FirstOrDefaultAsync(x => x.DeviceId == deviceId, cancellationToken); - return settings != null ? _mapper.Map(settings) : null; + if (settings == null) + return null; + + var dto = _mapper.Map(settings); + OrderRelays(dto); + return dto; } public async Task CreateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken) @@ -32,6 +39,8 @@ public sealed class DeviceSettingsService : IDeviceSettingsService entity.CreatedAt = DateTime.UtcNow; entity.UpdatedAt = DateTime.UtcNow; + SyncRelays(entity, dto.Relays ?? new List()); + _dbContext.DeviceSettings.Add(entity); await _dbContext.SaveChangesAsync(cancellationToken); return entity.Id; @@ -40,6 +49,8 @@ public sealed class DeviceSettingsService : IDeviceSettingsService public async Task UpdateAsync(DeviceSettingsDto dto, CancellationToken cancellationToken) { var entity = await _dbContext.DeviceSettings + .Include(x => x.Relays) + .ThenInclude(r => r.Rules) .FirstOrDefaultAsync(x => x.DeviceId == dto.DeviceId, cancellationToken); if (entity == null) @@ -48,6 +59,80 @@ public sealed class DeviceSettingsService : IDeviceSettingsService _mapper.Map(dto, entity); entity.UpdatedAt = DateTime.UtcNow; + SyncRelays(entity, dto.Relays ?? new List()); + await _dbContext.SaveChangesAsync(cancellationToken); } + + private static void OrderRelays(DeviceSettingsDto dto) + { + if (dto.Relays == null) + return; + + dto.Relays = dto.Relays + .OrderBy(r => r.RelayIndex) + .ToList(); + + foreach (var relay in dto.Relays) + { + relay.Rules = relay.Rules? + .OrderBy(r => r.Order) + .ToList() ?? new List(); + } + } + + private static void SyncRelays(Domain.DeviceSettings entity, List relayDtos) + { + var now = DateTime.UtcNow; + var dtoIndexes = relayDtos.Select(r => r.RelayIndex).ToHashSet(); + + // حذف رله‌هایی که دیگر در لیست نیستند + foreach (var relay in entity.Relays.Where(r => !dtoIndexes.Contains(r.RelayIndex)).ToList()) + { + entity.Relays.Remove(relay); + } + + foreach (var dto in relayDtos) + { + var relay = entity.Relays.FirstOrDefault(r => r.RelayIndex == dto.RelayIndex); + if (relay == null) + { + relay = new Domain.DeviceRelay + { + RelayIndex = dto.RelayIndex, + CreatedAt = now + }; + entity.Relays.Add(relay); + } + + relay.Name = dto.Name ?? string.Empty; + relay.IsEnabled = dto.IsEnabled; + relay.RelayKind = dto.RelayKind; + relay.Mode = dto.Mode; + relay.MinOnMinutes = dto.MinOnMinutes; + relay.MinOffMinutes = dto.MinOffMinutes; + relay.UpdatedAt = now; + + SyncRules(relay, dto.Rules ?? new List()); + } + } + + private static void SyncRules(Domain.DeviceRelay relay, List ruleDtos) + { + relay.Rules.Clear(); + + foreach (var dto in ruleDtos.OrderBy(r => r.Order)) + { + relay.Rules.Add(new Domain.RelayRule + { + SensorType = dto.SensorType, + ComparisonType = dto.ComparisonType, + Value1 = dto.Value1, + Value2 = dto.Value2, + Action = dto.Action, + TimeType = dto.TimeType, + Order = dto.Order + }); + } + } } diff --git a/src/GreenHome.Infrastructure/Services/Telemetry/TelemetryService.cs b/src/GreenHome.Infrastructure/Services/Telemetry/TelemetryService.cs index 1637d40..f4ca91f 100644 --- a/src/GreenHome.Infrastructure/Services/Telemetry/TelemetryService.cs +++ b/src/GreenHome.Infrastructure/Services/Telemetry/TelemetryService.cs @@ -76,8 +76,12 @@ public sealed class TelemetryService : ITelemetryService if (filter.EndDateUtc.HasValue) query = query.Where(x => x.ServerTimestampUtc < filter.EndDateUtc.Value.ToLocalTime()); + if (filter.SinceUtc.HasValue) + query = query.Where(x => x.ServerTimestampUtc > filter.SinceUtc.Value.ToLocalTime()); + if (filter.Page <= 0) filter.Page = 1; - if (filter.PageSize <= 0) filter.PageSize = 1000000; + if (filter.PageSize <= 0) filter.PageSize = 1000; + if (filter.PageSize > 50000) filter.PageSize = 50000; var total = await query.CountAsync(cancellationToken); var skip = (filter.Page - 1) * filter.PageSize; @@ -128,14 +132,20 @@ public sealed class TelemetryService : ITelemetryService { MinTemperatureC = result.Min(x => x.TemperatureC), MaxTemperatureC = result.Max(x => x.TemperatureC), + AvgTemperatureC = result.Average(x => x.TemperatureC), MinHumidityPercent = result.Min(x => x.HumidityPercent), MaxHumidityPercent = result.Max(x => x.HumidityPercent), + AvgHumidityPercent = result.Average(x => x.HumidityPercent), MinSoilPercent = result.Min(x => x.SoilPercent), MaxSoilPercent = result.Max(x => x.SoilPercent), + AvgSoilPercent = result.Average(x => x.SoilPercent), MinGasPPM = result.Min(x => x.GasPPM), MaxGasPPM = result.Max(x => x.GasPPM), + AvgGasPPM = (decimal)result.Average(x => x.GasPPM), MinLux = result.Min(x => x.Lux), - MaxLux = result.Max(x => x.Lux) + MaxLux = result.Max(x => x.Lux), + AvgLux = result.Average(x => x.Lux), + Count = result.Count }; } @@ -160,4 +170,59 @@ public sealed class TelemetryService : ITelemetryService .ToListAsync(cancellationToken); return months; } + + public async Task> GetAggregatedAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, int bucketMinutes, CancellationToken cancellationToken) + { + if (bucketMinutes <= 0) bucketMinutes = 15; + + var query = _dbContext.TelemetryRecords.AsNoTracking() + .Where(x => x.DeviceId == deviceId); + + if (startUtc.HasValue) + query = query.Where(x => x.ServerTimestampUtc >= startUtc.Value.ToLocalTime()); + if (endUtc.HasValue) + query = query.Where(x => x.ServerTimestampUtc < endUtc.Value.ToLocalTime()); + + var rows = await query + .OrderBy(x => x.ServerTimestampUtc) + .Select(x => new + { + x.ServerTimestampUtc, + x.TemperatureC, + x.HumidityPercent, + x.SoilPercent, + x.GasPPM, + x.Lux + }) + .ToListAsync(cancellationToken); + + if (rows.Count == 0) + return Array.Empty(); + + var bucketTicks = TimeSpan.FromMinutes(bucketMinutes).Ticks; + + return rows + .GroupBy(r => new DateTime(r.ServerTimestampUtc.Ticks - (r.ServerTimestampUtc.Ticks % bucketTicks))) + .Select(g => new TelemetryBucketDto + { + BucketStart = g.Key, + MinTemperatureC = g.Min(x => x.TemperatureC), + MaxTemperatureC = g.Max(x => x.TemperatureC), + AvgTemperatureC = g.Average(x => x.TemperatureC), + MinHumidityPercent = g.Min(x => x.HumidityPercent), + MaxHumidityPercent = g.Max(x => x.HumidityPercent), + AvgHumidityPercent = g.Average(x => x.HumidityPercent), + MinSoilPercent = g.Min(x => x.SoilPercent), + MaxSoilPercent = g.Max(x => x.SoilPercent), + AvgSoilPercent = g.Average(x => x.SoilPercent), + MinGasPPM = g.Min(x => x.GasPPM), + MaxGasPPM = g.Max(x => x.GasPPM), + AvgGasPPM = (decimal)g.Average(x => x.GasPPM), + MinLux = g.Min(x => x.Lux), + MaxLux = g.Max(x => x.Lux), + AvgLux = g.Average(x => x.Lux), + Count = g.Count() + }) + .ToList(); + } }