refactor: reorganize Infrastructure layer into folder structure with DI

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

View File

@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure; namespace GreenHome.Infrastructure.Data;
public sealed class GreenHomeDbContext : DbContext public sealed class GreenHomeDbContext : DbContext
{ {
@@ -26,6 +26,7 @@ public sealed class GreenHomeDbContext : DbContext
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>(); public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>(); public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>(); public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
public DbSet<Domain.PowerOutageLog> PowerOutageLogs => Set<Domain.PowerOutageLog>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -372,5 +373,22 @@ public sealed class GreenHomeDbContext : DbContext
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.DevicePostId); b.HasIndex(x => x.DevicePostId);
}); });
modelBuilder.Entity<Domain.PowerOutageLog>(b =>
{
b.ToTable("PowerOutageLogs");
b.HasKey(x => x.Id);
b.Property(x => x.PreviousPower).IsRequired();
b.Property(x => x.CurrentPower).IsRequired();
b.Property(x => x.EventType).IsRequired().HasConversion<int>();
b.Property(x => x.PersianDate).HasMaxLength(10);
b.HasOne(x => x.Device)
.WithMany()
.HasForeignKey(x => x.DeviceId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.DeviceId, x.TimestampUtc });
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
b.HasIndex(x => x.EventType);
});
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using GreenHome.Infrastructure; using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable #nullable disable
namespace GreenHome.Infrastructure.Migrations namespace GreenHome.Infrastructure.Data.Migrations
{ {
[DbContext(typeof(GreenHomeDbContext))] [DbContext(typeof(GreenHomeDbContext))]
partial class GreenHomeDbContextModelSnapshot : ModelSnapshot partial class GreenHomeDbContextModelSnapshot : ModelSnapshot
@@ -681,6 +681,54 @@ namespace GreenHome.Infrastructure.Migrations
b.ToTable("DeviceUsers", (string)null); b.ToTable("DeviceUsers", (string)null);
}); });
modelBuilder.Entity("GreenHome.Domain.PowerOutageLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<byte>("CurrentPower")
.HasColumnType("tinyint");
b.Property<int>("DeviceId")
.HasColumnType("int");
b.Property<int>("EventType")
.HasColumnType("int");
b.Property<string>("PersianDate")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<int>("PersianMonth")
.HasColumnType("int");
b.Property<int>("PersianYear")
.HasColumnType("int");
b.Property<byte>("PreviousPower")
.HasColumnType("tinyint");
b.Property<DateTime>("ServerTimestampUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("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.ReportImage", b => modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1174,6 +1222,17 @@ namespace GreenHome.Infrastructure.Migrations
b.Navigation("User"); 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.ReportImage", b => modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
{ {
b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport") b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport")

View File

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

View File

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

View File

@@ -1,38 +1,39 @@
using GreenHome.Application; using GreenHome.Application;
using GreenHome.Domain; using GreenHome.Domain;
using GreenHome.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace GreenHome.Infrastructure; namespace GreenHome.Infrastructure.Services.AI;
public sealed class AIQueryService : IAIQueryService public sealed class AIQueryService : IAIQueryService
{ {
private readonly GreenHomeDbContext dbContext; private readonly GreenHomeDbContext _dbContext;
private readonly ILogger<AIQueryService> logger; private readonly ILogger<AIQueryService> _logger;
public AIQueryService( public AIQueryService(
GreenHomeDbContext dbContext, GreenHomeDbContext dbContext,
ILogger<AIQueryService> logger) ILogger<AIQueryService> logger)
{ {
this.dbContext = dbContext; _dbContext = dbContext;
this.logger = logger; _logger = logger;
} }
public async Task<AIQuery> SaveQueryAsync(AIQuery query, CancellationToken cancellationToken = default) public async Task<AIQuery> SaveQueryAsync(AIQuery query, CancellationToken cancellationToken = default)
{ {
try try
{ {
dbContext.AIQueries.Add(query); _dbContext.AIQueries.Add(query);
await dbContext.SaveChangesAsync(cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AI query saved: {QueryId}, Tokens: {TotalTokens}", _logger.LogInformation("AI query saved: {QueryId}, Tokens: {TotalTokens}",
query.Id, query.TotalTokens); query.Id, query.TotalTokens);
return query; return query;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error saving AI query"); _logger.LogError(ex, "Error saving AI query");
throw; throw;
} }
} }
@@ -42,7 +43,7 @@ public sealed class AIQueryService : IAIQueryService
int take = 50, int take = 50,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await dbContext.AIQueries return await _dbContext.AIQueries
.Where(q => q.DeviceId == deviceId) .Where(q => q.DeviceId == deviceId)
.OrderByDescending(q => q.CreatedAt) .OrderByDescending(q => q.CreatedAt)
.Take(take) .Take(take)
@@ -54,7 +55,7 @@ public sealed class AIQueryService : IAIQueryService
int take = 50, int take = 50,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await dbContext.AIQueries return await _dbContext.AIQueries
.Where(q => q.UserId == userId) .Where(q => q.UserId == userId)
.OrderByDescending(q => q.CreatedAt) .OrderByDescending(q => q.CreatedAt)
.Take(take) .Take(take)
@@ -65,7 +66,7 @@ public sealed class AIQueryService : IAIQueryService
int deviceId, int deviceId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await dbContext.AIQueries return await _dbContext.AIQueries
.Where(q => q.DeviceId == deviceId) .Where(q => q.DeviceId == deviceId)
.SumAsync(q => q.TotalTokens, cancellationToken); .SumAsync(q => q.TotalTokens, cancellationToken);
} }
@@ -74,7 +75,7 @@ public sealed class AIQueryService : IAIQueryService
int userId, int userId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await dbContext.AIQueries return await _dbContext.AIQueries
.Where(q => q.UserId == userId) .Where(q => q.UserId == userId)
.SumAsync(q => q.TotalTokens, cancellationToken); .SumAsync(q => q.TotalTokens, cancellationToken);
} }
@@ -83,7 +84,7 @@ public sealed class AIQueryService : IAIQueryService
int take = 20, int take = 20,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await dbContext.AIQueries return await _dbContext.AIQueries
.Include(q => q.Device) .Include(q => q.Device)
.Include(q => q.User) .Include(q => q.User)
.OrderByDescending(q => q.CreatedAt) .OrderByDescending(q => q.CreatedAt)
@@ -95,7 +96,7 @@ public sealed class AIQueryService : IAIQueryService
{ {
var today = DateTime.UtcNow.Date; var today = DateTime.UtcNow.Date;
var allQueries = await dbContext.AIQueries var allQueries = await _dbContext.AIQueries
.Select(q => new .Select(q => new
{ {
q.TotalTokens, q.TotalTokens,
@@ -123,4 +124,3 @@ public sealed class AIQueryService : IAIQueryService
}; };
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
using AutoMapper; using AutoMapper;
using GreenHome.Application; using GreenHome.Application;
using GreenHome.Infrastructure.Data;
using GreenHome.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace GreenHome.Infrastructure; namespace GreenHome.Infrastructure.Services.Report;
public sealed class UserDailyReportService : IUserDailyReportService public sealed class UserDailyReportService : IUserDailyReportService
{ {
@@ -87,8 +89,8 @@ public sealed class UserDailyReportService : IUserDailyReportService
CreateUserDailyReportRequest request, CreateUserDailyReportRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// Validate date format // Validate Persian date format
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day)) if (!PersianDateHelper.TryParsePersianDate(request.PersianDate, out var year, out var month, out var day))
{ {
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد"); throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد");
} }
@@ -198,28 +200,4 @@ public sealed class UserDailyReportService : IUserDailyReportService
_context.ReportImages.Remove(image); _context.ReportImages.Remove(image);
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
} }
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
{
year = month = day = 0;
if (string.IsNullOrWhiteSpace(persianDate))
return false;
var parts = persianDate.Split('/');
if (parts.Length != 3)
return false;
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
return false;
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
return false;
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
return false;
return true;
} }
}

View File

@@ -1,6 +1,6 @@
using GreenHome.Application; using GreenHome.Application;
namespace GreenHome.Infrastructure; namespace GreenHome.Infrastructure.Services.Sun;
/// <summary> /// <summary>
/// سرویس محاسبه طلوع و غروب خورشید /// سرویس محاسبه طلوع و غروب خورشید
@@ -94,7 +94,7 @@ public sealed class SunCalculatorService : ISunCalculatorService
return (sunrise, sunset); return (sunrise, sunset);
} }
private double CalculateJulianDay(DateTime date) private static double CalculateJulianDay(DateTime date)
{ {
var year = date.Year; var year = date.Year;
var month = date.Month; var month = date.Month;
@@ -112,14 +112,13 @@ public sealed class SunCalculatorService : ISunCalculatorService
return Math.Floor(365.25 * (year + 4716)) + Math.Floor(30.6001 * (month + 1)) + day + b - 1524.5; return Math.Floor(365.25 * (year + 4716)) + Math.Floor(30.6001 * (month + 1)) + day + b - 1524.5;
} }
private double ToRadians(double degrees) private static double ToRadians(double degrees)
{ {
return degrees * Math.PI / 180.0; return degrees * Math.PI / 180.0;
} }
private double ToDegrees(double radians) private static double ToDegrees(double radians)
{ {
return radians * 180.0 / Math.PI; return radians * 180.0 / Math.PI;
} }
} }

View File

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

View File

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