feat: add device relay and telemetry support
This commit is contained in:
@@ -20,9 +20,9 @@ public class TelemetryController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<TelemetryDto>>> List([FromQuery] int? deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default)
|
||||
public async Task<ActionResult<PagedResult<TelemetryDto>>> 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<ActionResult<IReadOnlyList<TelemetryBucketDto>>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,17 @@ public sealed class RelayConditionDto
|
||||
{
|
||||
public int Enabled { get; set; }
|
||||
public int TimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه رله برای سازگاری با فریمور قبلی (مثلاً R1)
|
||||
/// </summary>
|
||||
public string Relay { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// نام نمایشی رله (فن، هیتر و ...)
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<RelayRuleDto> 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; }
|
||||
|
||||
/// <summary>
|
||||
/// اقدام: 0 = روشن، 1 = خاموش
|
||||
/// </summary>
|
||||
public int Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان اعمال: 0 = روز، 1 = شب، 2 = همیشه
|
||||
/// </summary>
|
||||
public int TimeType { get; set; }
|
||||
}
|
||||
|
||||
35
src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs
Normal file
35
src/GreenHome.Application/Dtos/Device/DeviceRelayDto.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Domain = GreenHome.Domain;
|
||||
|
||||
namespace GreenHome.Application;
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات یک رله دستگاه
|
||||
/// </summary>
|
||||
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<DeviceRelayRuleDto> Rules { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// قانون روشن/خاموش شدن رله
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@@ -25,6 +25,8 @@ public sealed class DeviceSettingsDto
|
||||
public string? VerificationCode { get; set; }
|
||||
public DateTime? TokenExpiresAt { get; set; }
|
||||
|
||||
public List<DeviceRelayDto> Relays { get; set; } = new();
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ public interface ITelemetryService
|
||||
Task<TelemetryMinMax> GetMinMaxAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<DayCount>> GetMonthDaysAsync(int deviceId, int persianYear, int persianMonth, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<int>> GetActiveMonthsAsync(int deviceId, int persianYear, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<TelemetryBucketDto>> GetAggregatedAsync(int deviceId, DateTime? startUtc, DateTime? endUtc, int bucketMinutes, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,17 @@ public sealed class MappingProfile : Profile
|
||||
CreateMap<Domain.DeviceSettings, DeviceSettingsDto>()
|
||||
.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<Domain.DeviceRelay, DeviceRelayDto>()
|
||||
.ReverseMap()
|
||||
.ForMember(dest => dest.DeviceSettings, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.Rules, opt => opt.Ignore());
|
||||
|
||||
CreateMap<Domain.RelayRule, DeviceRelayRuleDto>()
|
||||
.ReverseMap()
|
||||
.ForMember(dest => dest.DeviceRelay, opt => opt.Ignore());
|
||||
|
||||
CreateMap<Domain.AlertCondition, AlertConditionDto>()
|
||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||
|
||||
120
src/GreenHome.Domain/DeviceRelay.cs
Normal file
120
src/GreenHome.Domain/DeviceRelay.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// نوع مصرفکننده متصل به رله
|
||||
/// </summary>
|
||||
public enum RelayKind
|
||||
{
|
||||
/// <summary>سایر / نامشخص</summary>
|
||||
Generic = 0,
|
||||
|
||||
/// <summary>فن</summary>
|
||||
Fan = 1,
|
||||
|
||||
/// <summary>هیتر / بخاری</summary>
|
||||
Heater = 2,
|
||||
|
||||
/// <summary>پمپ آب</summary>
|
||||
WaterPump = 3,
|
||||
|
||||
/// <summary>مهپاش / رطوبتساز</summary>
|
||||
Humidifier = 4,
|
||||
|
||||
/// <summary>روشنایی</summary>
|
||||
Light = 5,
|
||||
|
||||
/// <summary>شیر برقی</summary>
|
||||
Valve = 6
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حالت عملکرد رله
|
||||
/// </summary>
|
||||
public enum RelayMode
|
||||
{
|
||||
/// <summary>خودکار (بر اساس قوانین)</summary>
|
||||
Auto = 0,
|
||||
|
||||
/// <summary>دستی (قوانین خودکار نادیده گرفته میشوند)</summary>
|
||||
Manual = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نوع اقدام یک قانون رله
|
||||
/// </summary>
|
||||
public enum RelayAction
|
||||
{
|
||||
/// <summary>روشن کردن</summary>
|
||||
On = 0,
|
||||
|
||||
/// <summary>خاموش کردن</summary>
|
||||
Off = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// رله دستگاه به همراه تنظیمات آن
|
||||
/// </summary>
|
||||
public sealed class DeviceRelay
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceSettingsId { get; set; }
|
||||
public DeviceSettings DeviceSettings { get; set; } = null!;
|
||||
|
||||
/// <summary>شماره رله (۱ تا RelayCount)</summary>
|
||||
public int RelayIndex { get; set; }
|
||||
|
||||
/// <summary>نام نمایشی رله (مثلاً فن، هیتر)</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>آیا این رله فعال است؟</summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>نوع مصرفکننده متصل به رله</summary>
|
||||
public RelayKind RelayKind { get; set; } = RelayKind.Generic;
|
||||
|
||||
/// <summary>حالت عملکرد: خودکار یا دستی</summary>
|
||||
public RelayMode Mode { get; set; } = RelayMode.Auto;
|
||||
|
||||
/// <summary>حداقل زمان روشن ماندن (دقیقه) - برای جلوگیری از قطع و وصل مکرر</summary>
|
||||
public int MinOnMinutes { get; set; } = 0;
|
||||
|
||||
/// <summary>حداقل زمان خاموش ماندن (دقیقه) - برای محافظت از تجهیزات</summary>
|
||||
public int MinOffMinutes { get; set; } = 0;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>قوانین روشن/خاموش شدن رله</summary>
|
||||
public ICollection<RelayRule> Rules { get; set; } = new List<RelayRule>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// یک قانون برای روشن یا خاموش کردن رله بر اساس سنسورها
|
||||
/// </summary>
|
||||
public sealed class RelayRule
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceRelayId { get; set; }
|
||||
public DeviceRelay DeviceRelay { get; set; } = null!;
|
||||
|
||||
/// <summary>نوع سنسور: دما، رطوبت، خاک، گاز، نور</summary>
|
||||
public SensorType SensorType { get; set; }
|
||||
|
||||
/// <summary>نوع مقایسه: بزرگتر، کوچکتر، بین، خارج از محدوده</summary>
|
||||
public ComparisonType ComparisonType { get; set; }
|
||||
|
||||
/// <summary>مقدار عددی اول</summary>
|
||||
public decimal Value1 { get; set; }
|
||||
|
||||
/// <summary>مقدار عددی دوم (برای بین و خارج از محدوده)</summary>
|
||||
public decimal? Value2 { get; set; }
|
||||
|
||||
/// <summary>اقدام: روشن یا خاموش کردن رله</summary>
|
||||
public RelayAction Action { get; set; } = RelayAction.On;
|
||||
|
||||
/// <summary>زمان اعمال: روز، شب، همیشه</summary>
|
||||
public AlertTimeType TimeType { get; set; } = AlertTimeType.Always;
|
||||
|
||||
/// <summary>ترتیب قوانین</summary>
|
||||
public int Order { get; set; }
|
||||
}
|
||||
@@ -109,4 +109,9 @@ public sealed class DeviceSettings
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات هر رله دستگاه
|
||||
/// </summary>
|
||||
public ICollection<DeviceRelay> Relays { get; set; } = new List<DeviceRelay>();
|
||||
}
|
||||
@@ -11,6 +11,8 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
|
||||
public DbSet<Domain.AlertCondition> AlertConditions => Set<Domain.AlertCondition>();
|
||||
public DbSet<Domain.AlertRule> AlertRules => Set<Domain.AlertRule>();
|
||||
public DbSet<Domain.DeviceRelay> DeviceRelays => Set<Domain.DeviceRelay>();
|
||||
public DbSet<Domain.RelayRule> RelayRules => Set<Domain.RelayRule>();
|
||||
public DbSet<Domain.User> Users => Set<Domain.User>();
|
||||
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||
@@ -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<Domain.DeviceRelay>(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<int>();
|
||||
b.Property(x => x.Mode).IsRequired().HasConversion<int>();
|
||||
b.HasIndex(x => new { x.DeviceSettingsId, x.RelayIndex }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.RelayRule>(b =>
|
||||
{
|
||||
b.ToTable("RelayRules");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.SensorType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.ComparisonType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Action).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.TimeType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Value1).IsRequired().HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.Value2).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.Order).IsRequired();
|
||||
b.HasOne(x => x.DeviceRelay)
|
||||
.WithMany(r => r.Rules)
|
||||
.HasForeignKey(x => x.DeviceRelayId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.DeviceRelayId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AlertCondition>(b =>
|
||||
|
||||
1429
src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.Designer.cs
generated
Normal file
1429
src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260903014200_AddDeviceRelays.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Data.Migrations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeviceRelays : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DeviceRelays",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceSettingsId = table.Column<int>(type: "int", nullable: false),
|
||||
RelayIndex = table.Column<int>(type: "int", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
RelayKind = table.Column<int>(type: "int", nullable: false),
|
||||
Mode = table.Column<int>(type: "int", nullable: false),
|
||||
MinOnMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
MinOffMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(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<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceRelayId = table.Column<int>(type: "int", nullable: false),
|
||||
SensorType = table.Column<int>(type: "int", nullable: false),
|
||||
ComparisonType = table.Column<int>(type: "int", nullable: false),
|
||||
Value1 = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||
Value2 = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
|
||||
Action = table.Column<int>(type: "int", nullable: false),
|
||||
TimeType = table.Column<int>(type: "int", nullable: false),
|
||||
Order = table.Column<int>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "RelayRules");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DeviceRelays");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceSettingsId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("MinOffMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MinOnMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Mode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("RelayIndex")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RelayKind")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceSettingsId", "RelayIndex")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceRelays", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -729,6 +777,45 @@ namespace GreenHome.Infrastructure.Data.Migrations
|
||||
b.ToTable("PowerOutageLogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.RelayRule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Action")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ComparisonType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DeviceRelayId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SensorType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TimeType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Value1")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal?>("Value2")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceRelayId");
|
||||
|
||||
b.ToTable("RelayRules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
|
||||
{
|
||||
b.Property<int>("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");
|
||||
|
||||
@@ -123,24 +123,29 @@ public sealed class AlertConditionService : IAlertConditionService
|
||||
|
||||
public async Task<IReadOnlyList<RelayConditionDto>> 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<RelayConditionDto>();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DeviceSettingsDto>(settings) : null;
|
||||
if (settings == null)
|
||||
return null;
|
||||
|
||||
var dto = _mapper.Map<DeviceSettingsDto>(settings);
|
||||
OrderRelays(dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<int> 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<DeviceRelayDto>());
|
||||
|
||||
_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<DeviceRelayDto>());
|
||||
|
||||
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<DeviceRelayRuleDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncRelays(Domain.DeviceSettings entity, List<DeviceRelayDto> 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<DeviceRelayRuleDto>());
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncRules(Domain.DeviceRelay relay, List<DeviceRelayRuleDto> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IReadOnlyList<TelemetryBucketDto>> 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<TelemetryBucketDto>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user