Compare commits

...

5 Commits

113 changed files with 5512 additions and 3639 deletions

View File

@@ -8,13 +8,16 @@ namespace GreenHome.Api.Controllers;
public class PowerOutageController : ControllerBase public class PowerOutageController : ControllerBase
{ {
private readonly IAlertService _alertService; private readonly IAlertService _alertService;
private readonly IPowerOutageLogService _powerOutageLogService;
private readonly ILogger<PowerOutageController> _logger; private readonly ILogger<PowerOutageController> _logger;
public PowerOutageController( public PowerOutageController(
IAlertService alertService, IAlertService alertService,
IPowerOutageLogService powerOutageLogService,
ILogger<PowerOutageController> logger) ILogger<PowerOutageController> logger)
{ {
_alertService = alertService; _alertService = alertService;
_powerOutageLogService = powerOutageLogService;
_logger = logger; _logger = logger;
} }
@@ -56,5 +59,175 @@ public class PowerOutageController : ControllerBase
return StatusCode(500, new { error = "خطا در ارسال هشدار قطع برق" }); return StatusCode(500, new { error = "خطا در ارسال هشدار قطع برق" });
} }
} }
/// <summary>
/// دریافت لیست رویدادهای قطع و وصل برق یک دستگاه
/// </summary>
/// <param name="deviceId">شناسه دستگاه</param>
/// <param name="startUtc">شروع بازه (UTC)</param>
/// <param name="endUtc">پایان بازه (UTC)</param>
/// <param name="eventType">نوع رویداد: 1 = وصل, 2 = قطع (اختیاری)</param>
/// <param name="page">شماره صفحه</param>
/// <param name="pageSize">تعداد در هر صفحه</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>لیست رویدادها</returns>
[HttpGet("logs")]
public async Task<ActionResult<PagedResult<PowerOutageLogDto>>> GetLogs(
[FromQuery] int deviceId,
[FromQuery] DateTime? startUtc,
[FromQuery] DateTime? endUtc,
[FromQuery] int? eventType,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken cancellationToken = default)
{
try
{
if (deviceId <= 0)
{
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
}
var filter = new PowerOutageFilter
{
EventType = eventType,
StartDateUtc = startUtc,
EndDateUtc = endUtc,
Page = page,
PageSize = pageSize
};
var result = await _powerOutageLogService.ListAsync(deviceId, filter, cancellationToken);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting power outage logs: DeviceId={DeviceId}", deviceId);
return StatusCode(500, new { error = "خطا در دریافت لیست رویدادهای برق" });
}
}
/// <summary>
/// دریافت خلاصه قطعی‌های برق یک دستگاه در یک بازه زمانی
/// </summary>
/// <param name="deviceId">شناسه دستگاه</param>
/// <param name="startUtc">شروع بازه (UTC)</param>
/// <param name="endUtc">پایان بازه (UTC)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>خلاصه قطعی‌ها شامل تعداد قطعی‌ها و مجموع مدت زمان قطعی</returns>
[HttpGet("summary")]
public async Task<ActionResult<PowerOutageSummaryDto>> GetSummary(
[FromQuery] int deviceId,
[FromQuery] DateTime? startUtc,
[FromQuery] DateTime? endUtc,
CancellationToken cancellationToken = default)
{
try
{
if (deviceId <= 0)
{
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
}
var filter = new PowerOutageFilter
{
StartDateUtc = startUtc,
EndDateUtc = endUtc,
PageSize = int.MaxValue // Get all records for summary
};
var result = await _powerOutageLogService.ListAsync(deviceId, filter, cancellationToken);
var outages = result.Items.Where(x => x.CurrentPower == 0).ToList();
var connections = result.Items.Where(x => x.CurrentPower == 1).ToList();
// Calculate total downtime by pairing disconnects with subsequent connects
long totalDowntimeSeconds = 0;
var pairedOutages = new List<PowerOutagePairDto>();
foreach (var outage in outages)
{
// Find the next connect event after this outage
var nextConnect = connections
.Where(c => c.TimestampUtc > outage.TimestampUtc)
.OrderBy(c => c.TimestampUtc)
.FirstOrDefault();
if (nextConnect != null)
{
var duration = (nextConnect.TimestampUtc - outage.TimestampUtc).TotalSeconds;
totalDowntimeSeconds += (long)duration;
pairedOutages.Add(new PowerOutagePairDto
{
DisconnectedAt = outage.TimestampUtc,
ConnectedAt = nextConnect.TimestampUtc,
DurationSeconds = (long)duration
});
}
else
{
// Still disconnected (no subsequent connect)
pairedOutages.Add(new PowerOutagePairDto
{
DisconnectedAt = outage.TimestampUtc,
ConnectedAt = null,
DurationSeconds = null
});
}
}
var summary = new PowerOutageSummaryDto
{
DeviceId = deviceId,
TotalOutageCount = outages.Count,
TotalDowntimeSeconds = totalDowntimeSeconds,
StartDateUtc = startUtc,
EndDateUtc = endUtc,
Outages = pairedOutages
};
return Ok(summary);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting power outage summary: DeviceId={DeviceId}", deviceId);
return StatusCode(500, new { error = "خطا در دریافت خلاصه قطعی‌های برق" });
}
}
/// <summary>
/// مهاجرت داده‌های قدیمی از جدول Telemetry به PowerOutageLogs
/// این اندپوینت یکبار مصرف است - بعد از اجرا می‌توانید آن را غیرفعال کنید
/// </summary>
/// <param name="deviceId">اختیاری - اگر داده شود فقط برای همان دستگاه مهاجرت می‌کند</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>تعداد رکوردهای مهاجرت داده شده</returns>
[HttpPost("migrate")]
public async Task<ActionResult> MigrateFromTelemetry(
[FromQuery] int? deviceId,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Starting migration from Telemetry to PowerOutageLogs...");
var count = await _powerOutageLogService.MigrateFromTelemetryAsync(deviceId, cancellationToken);
_logger.LogInformation("Migration completed: {Count} records inserted", count);
return Ok(new
{
success = true,
message = $"مهاجرت با موفقیت انجام شد. {count} رکورد به جدول PowerOutageLogs اضافه شد.",
recordsInserted = count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during migration from Telemetry to PowerOutageLogs");
return StatusCode(500, new { error = $"خطا در مهاجرت داده‌ها: {ex.Message}" });
}
}
} }

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using GreenHome.Application; using GreenHome.Application;
using System.Text;
namespace GreenHome.Api.Controllers; namespace GreenHome.Api.Controllers;
@@ -29,38 +30,66 @@ public class TelemetryController : ControllerBase
[HttpGet("AddData")] [HttpGet("AddData")]
public async Task<ActionResult<string>> Create([FromQuery] TelemetryDto telementry, [FromQuery] string? lastUpdateDate, CancellationToken cancellationToken) public async Task<ActionResult<string>> Create([FromQuery] TelemetryDto telementry, [FromQuery] string? lastUpdateDate, CancellationToken cancellationToken)
{ {
telementry.TimestampUtc = DateTime.UtcNow; telementry.TimestampUtc = DateTime.UtcNow;
var id = await telemetryService.AddAsync(telementry, cancellationToken); var id = await telemetryService.AddAsync(telementry, cancellationToken);
string? message; var parts = new List<string> { $"ID:{id}" };
var deviceId = telementry.DeviceId; var deviceId = telementry.DeviceId;
if (deviceId > 0) if (deviceId > 0)
{ {
message = await alertService.CheckAndSendAlertsAsync(deviceId, telementry, cancellationToken); // 1. Check alerts and power status
if(!string.IsNullOrEmpty(message)) var alertResult = await alertService.CheckAndSendAlertsAsync(deviceId, telementry, cancellationToken);
if (alertResult != null)
{ {
return Ok(message); if (alertResult.Messages.Count > 0)
{
parts.Add($"A:{alertResult.Messages.Count}");
parts.Add($"R:{alertResult.Recipients}");
parts.Add($"M:{string.Join("@", alertResult.Messages)}");
}
if (alertResult.PowerStatus != null)
{
parts.Add($"P:{alertResult.PowerStatus}");
} }
} }
// Check if relay conditions have been updated since lastUpdateDate // 2. Check relay conditions update
if (deviceId > 0 && !string.IsNullOrEmpty(lastUpdateDate)) if (!string.IsNullOrEmpty(lastUpdateDate))
{ {
var parsedDate = ParseLastUpdateDate(lastUpdateDate); var parsedDate = ParseLastUpdateDate(lastUpdateDate);
// var relayConditions = await alertConditionService.GetUpdatedRelayConditionsAsync(deviceId, parsedDate, cancellationToken); var relayConditions = await alertConditionService.GetUpdatedRelayConditionsAsync(deviceId, parsedDate, cancellationToken);
// if (relayConditions.Count > 0) if (relayConditions.Count > 0)
// { {
// return Ok(new { conditions = relayConditions }); parts.Add("RU:1");
// } parts.Add($"RL:{relayConditions.Count}");
parts.Add($"RC:{EncodeRelayConditions(relayConditions)}");
var hasUpdates = await alertConditionService.HasUpdatedConditionsAsync(deviceId, parsedDate, cancellationToken); }
if(hasUpdates) }
return Ok("Relay");
} }
return Ok(id + ""); return Ok(string.Join("|", parts));
}
/// <summary>
/// Encode relay conditions into a compact string format for STM32 parsing.
/// Format: E{enabled}T{timeType}R{relay}S{sensor}O{op}V{value1}[,{value2}]
/// Multiple conditions separated by |, multiple rules within a condition separated by &
/// </summary>
private static string EncodeRelayConditions(IReadOnlyList<RelayConditionDto> conditions)
{
return string.Join("|", conditions.Select(c =>
{
var rules = string.Join("&", c.Rules.Select(r =>
{
var rule = $"S{r.Sensor}O{r.Op}V{r.V1}";
if (r.V2.HasValue)
rule += $",{r.V2.Value}";
return rule;
}));
return $"E{c.Enabled}T{c.TimeType}R{c.Relay}{rules}";
}));
} }
/// <summary> /// <summary>

View File

@@ -2,32 +2,63 @@ using FluentValidation;
using GreenHome.AI.DeepSeek; using GreenHome.AI.DeepSeek;
using GreenHome.Application; using GreenHome.Application;
using GreenHome.Infrastructure; using GreenHome.Infrastructure;
using GreenHome.Infrastructure.Data;
using GreenHome.Sms.Ippanel; using GreenHome.Sms.Ippanel;
using GreenHome.VoiceCall.Avanak; using GreenHome.VoiceCall.Avanak;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Scalar.AspNetCore; using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // ============================================================
// 1. Service Registration
// ============================================================
ConfigureCoreServices(builder);
ConfigureCors(builder);
ConfigureExternalServices(builder);
var app = builder.Build();
// ============================================================
// 2. Pipeline Configuration
// ============================================================
ApplyMigrations(app);
ConfigureMiddleware(app);
ConfigureStaticFiles(app);
app.MapControllers();
app.Run();
// ============================================================
// Service Registration Methods
// ============================================================
void ConfigureCoreServices(WebApplicationBuilder builder)
{
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
// Application/Infrastructure DI
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile)); builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>(); builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
// CORS Configuration builder.Services.AddInfrastructure(builder.Configuration);
}
void ConfigureCors(WebApplicationBuilder builder)
{
const string CorsPolicy = "DefaultCors"; const string CorsPolicy = "DefaultCors";
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy(CorsPolicy, policy => options.AddPolicy(CorsPolicy, policy =>
{ {
if (builder.Environment.IsDevelopment()) if (builder.Environment.IsDevelopment())
{ {
// در محیط Development همه origin ها مجاز هستند
policy policy
.AllowAnyOrigin() .AllowAnyOrigin()
.AllowAnyHeader() .AllowAnyHeader()
@@ -35,7 +66,6 @@ builder.Services.AddCors(options =>
} }
else else
{ {
// در محیط Production فقط دامنه‌های مشخص
policy policy
.WithOrigins( .WithOrigins(
"http://green.nabaksoft.ir", "http://green.nabaksoft.ir",
@@ -49,45 +79,27 @@ builder.Services.AddCors(options =>
} }
}); });
}); });
}
builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(options => void ConfigureExternalServices(WebApplicationBuilder builder)
{ {
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
});
builder.Services.AddScoped<GreenHome.Application.IDeviceService, GreenHome.Infrastructure.DeviceService>();
builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.Infrastructure.TelemetryService>();
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
builder.Services.AddScoped<GreenHome.Application.IAlertService, GreenHome.Infrastructure.AlertService>();
builder.Services.AddScoped<GreenHome.Application.IAlertConditionService, GreenHome.Infrastructure.AlertConditionService>();
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
builder.Services.AddScoped<GreenHome.Application.IDailyReportService, GreenHome.Infrastructure.DailyReportService>();
builder.Services.AddScoped<GreenHome.Application.IAlertLogService, GreenHome.Infrastructure.AlertLogService>();
builder.Services.AddScoped<GreenHome.Application.IUserDailyReportService, GreenHome.Infrastructure.UserDailyReportService>();
builder.Services.AddScoped<GreenHome.Application.IChecklistService, GreenHome.Infrastructure.ChecklistService>();
builder.Services.AddScoped<GreenHome.Application.IMonthlyReportService, GreenHome.Infrastructure.MonthlyReportService>();
builder.Services.AddScoped<GreenHome.Application.IDevicePostService, GreenHome.Infrastructure.DevicePostService>();
builder.Services.AddScoped<GreenHome.Application.IDeviceTokenService, GreenHome.Infrastructure.DeviceTokenService>();
// SMS Service Configuration
builder.Services.AddIppanelSms(builder.Configuration); builder.Services.AddIppanelSms(builder.Configuration);
// Voice Call Service Configuration
builder.Services.AddAvanakVoiceCall(builder.Configuration); builder.Services.AddAvanakVoiceCall(builder.Configuration);
// AI Service Configuration
builder.Services.AddDeepSeek(builder.Configuration); builder.Services.AddDeepSeek(builder.Configuration);
}
var app = builder.Build(); // ============================================================
// Middleware & Pipeline Methods
// ============================================================
// Apply pending migrations automatically void ApplyMigrations(WebApplication app)
using (var scope = app.Services.CreateScope())
{ {
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider; var services = scope.ServiceProvider;
try try
{ {
var context = services.GetRequiredService<GreenHome.Infrastructure.GreenHomeDbContext>(); var context = services.GetRequiredService<GreenHomeDbContext>();
context.Database.Migrate(); context.Database.Migrate();
} }
catch (Exception ex) catch (Exception ex)
@@ -97,28 +109,30 @@ using (var scope = app.Services.CreateScope())
} }
} }
// Configure the HTTP request pipeline. void ConfigureMiddleware(WebApplication app)
{
app.MapOpenApi(); app.MapOpenApi();
app.MapScalarApiReference(); app.MapScalarApiReference();
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseHttpsRedirection(); app.UseHttpsRedirection();
} }
app.UseCors(CorsPolicy); app.UseCors("DefaultCors");
app.UseAuthorization(); app.UseAuthorization();
}
void ConfigureStaticFiles(WebApplication app)
{
var provider = new FileExtensionContentTypeProvider(); var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".amr"] = "audio/amr"; provider.Mappings[".amr"] = "audio/amr";
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
FileProvider = new PhysicalFileProvider( FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "My_StaticFiles")), Path.Combine(app.Environment.ContentRootPath, "My_StaticFiles")),
RequestPath = "/My_StaticFiles", RequestPath = "/My_StaticFiles",
ContentTypeProvider = provider ContentTypeProvider = provider
}); });
}
app.MapControllers();
app.Run();

View File

@@ -6,7 +6,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"Default": "Server=87.107.108.119;TrustServerCertificate=True;Database=GreenHomeDb;User Id=sa;Password=qwER12#$110" "Default": "Server=.;Integrated Security=False;Trusted_Connection=True;Encrypt=false;MultipleActiveResultSets=true;Database=GreenHomeDb;"
}, },
"AvanakVoiceCall": { "AvanakVoiceCall": {
"Token": "A948B776B90CFD919B0EC60929714136CCB49DDB" "Token": "A948B776B90CFD919B0EC60929714136CCB49DDB"

View File

@@ -1,409 +0,0 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
// DTOs
public sealed class DeviceDto
{
public int Id { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserFamily { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string NeshanLocation { get; set; } = string.Empty;
}
public sealed class TelemetryDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string? DeviceName { get; set; }
public DateTime? TimestampUtc { get; set; }
public decimal TemperatureC { get; set; }
public decimal HumidityPercent { get; set; }
public decimal SoilPercent { get; set; }
public int GasPPM { get; set; }
public decimal? Voltage { get; set; }
public decimal? BatteryVoltage { get; set; }
public decimal? BatteryPercent { get; set; }
public byte? Power { get; set; }
public byte? OldPower { get; set; }
public decimal Lux { get; set; }
public int? PersianYear { get; set; }
public int? PersianMonth { get; set; }
public string PersianDate { get; set; } = string.Empty;
public DateTime? ServerTimestampUtc { get; set; }
}
public sealed class TelemetryFilter
{
public int? DeviceId { get; set; }
public DateTime? StartDateUtc { get; set; }
public DateTime? EndDateUtc { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class PagedResult<T>
{
public required IReadOnlyList<T> Items { get; init; }
public required int TotalCount { get; init; }
public required int Page { get; init; }
public required int PageSize { get; init; }
}
public sealed class TelemetryMinMax
{
public decimal MinTemperatureC { get; set; }
public decimal MaxTemperatureC { get; set; }
public decimal MinHumidityPercent { get; set; }
public decimal MaxHumidityPercent { get; set; }
public decimal MinSoilPercent { get; set; }
public decimal MaxSoilPercent { get; set; }
public int MinGasPPM { get; set; }
public int MaxGasPPM { get; set; }
public decimal MinLux { get; set; }
public decimal MaxLux { get; set; }
}
public sealed class DayCount
{
public string PersianDate { get; set; } = string.Empty;
public int Count { get; set; }
}
public sealed class SendCodeRequest
{
public required string Mobile { get; set; }
}
public sealed class SendCodeResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public int ResendAfterSeconds { get; set; } = 120;
}
public sealed class VerifyCodeRequest
{
public required string Mobile { get; set; }
public required string Code { get; set; }
}
public sealed class VerifyCodeResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? Token { get; set; }
public UserDto? User { get; set; }
}
public sealed class UserDto
{
public int Id { get; set; }
public string Mobile { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public Domain.UserRole Role { get; set; }
}
public sealed class DeviceFilter
{
public int? UserId { get; set; }
public string? Search { get; set; } // جستجو در نام دستگاه، نام صاحب، نام خانوادگی صاحب، Location
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
public sealed class DeviceSettingsDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string Province { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
public string ProductType { get; set; } = string.Empty;
public int MinimumSmsIntervalMinutes { get; set; } = 15;
public int MinimumCallIntervalMinutes { get; set; } = 60;
public decimal? AreaSquareMeters { get; set; }
public int UploadIntervalMin { get; set; } = 5;
public string DevicePhoneNumber { get; set; } = string.Empty;
public Domain.SimCardType? SimCardType { get; set; }
public int RelayCount { get; set; } = 4;
public string SmsRecievers { get; set; } = string.Empty;
public string? TokenCode { get; set; }
public string? VerificationCode { get; set; }
public DateTime? TokenExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public sealed class AlertConditionDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public Domain.AlertNotificationType NotificationType { get; set; }
public Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public List<AlertRuleDto> Rules { get; set; } = new();
}
public sealed class AlertRuleDto
{
public int Id { get; set; }
public int AlertConditionId { get; set; }
public Domain.SensorType SensorType { get; set; }
public Domain.ComparisonType ComparisonType { get; set; }
public decimal Value1 { get; set; }
public decimal? Value2 { get; set; }
public int Order { get; set; }
}
/// <summary>
/// DTO for relay alert conditions (notificationType > 0)
/// </summary>
public sealed class RelayConditionDto
{
public int Enabled { get; set; }
public int TimeType { get; set; }
public string Relay { get; set; } = string.Empty;
public List<RelayRuleDto> Rules { get; set; } = new();
}
/// <summary>
/// DTO for relay condition rules
/// </summary>
public sealed class RelayRuleDto
{
public string Sensor { get; set; } = string.Empty;
public string Op { get; set; } = string.Empty;
public decimal V1 { get; set; }
public decimal? V2 { get; set; }
}
public sealed class CreateAlertConditionRequest
{
public required int DeviceId { get; set; }
public required Domain.AlertNotificationType NotificationType { get; set; }
public required Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public required List<CreateAlertRuleRequest> Rules { get; set; }
}
public sealed class CreateAlertRuleRequest
{
public required Domain.SensorType SensorType { get; set; }
public required Domain.ComparisonType ComparisonType { get; set; }
public required decimal Value1 { get; set; }
public decimal? Value2 { get; set; }
public int Order { get; set; }
}
public sealed class UpdateAlertConditionRequest
{
public required int Id { get; set; }
public required Domain.AlertNotificationType NotificationType { get; set; }
public required Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public required List<CreateAlertRuleRequest> Rules { get; set; }
}
public sealed class DailyReportRequest
{
public required int DeviceId { get; set; }
public required string PersianDate { get; set; } // yyyy/MM/dd
}
public sealed class DailyReportResponse
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string PersianDate { get; set; } = string.Empty;
public string Analysis { get; set; } = string.Empty;
public int RecordCount { get; set; }
public int SampledRecordCount { get; set; }
public int TotalTokens { get; set; }
public DateTime CreatedAt { get; set; }
public bool FromCache { get; set; }
}
public sealed class WeeklyAnalysisRequest
{
public required int DeviceId { get; set; }
public required string StartDate { get; set; } // yyyy/MM/dd
public required string EndDate { get; set; } // yyyy/MM/dd
}
public sealed class MonthlyAnalysisRequest
{
public required int DeviceId { get; set; }
public required int Year { get; set; }
public required int Month { get; set; }
}
public sealed class AlertLogDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public int? AlertConditionId { get; set; }
public Domain.AlertType AlertType { get; set; }
public Domain.AlertNotificationType NotificationType { get; set; }
public string Message { get; set; } = string.Empty;
public Domain.AlertStatus Status { get; set; }
public string? ErrorMessage { get; set; }
public string PhoneNumber { get; set; } = string.Empty;
public DateTime SentAt { get; set; }
public long ProcessingTimeMs { get; set; }
}
public sealed class AlertLogFilter
{
public int? DeviceId { get; set; }
public int? UserId { get; set; }
public Domain.AlertType? AlertType { get; set; }
public Domain.AlertStatus? Status { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public sealed class UserDailyReportDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserFamily { get; set; } = string.Empty;
public string PersianDate { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Observations { get; set; } = string.Empty;
public string Operations { get; set; } = string.Empty;
public string? Notes { get; set; }
public List<ReportImageDto> Images { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public sealed class ReportImageDto
{
public int Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public string ContentType { get; set; } = string.Empty;
public long FileSize { get; set; }
public string? Description { get; set; }
public DateTime UploadedAt { get; set; }
}
public sealed class CreateUserDailyReportRequest
{
public required int DeviceId { get; set; }
public required int UserId { get; set; }
public required string PersianDate { get; set; }
public required string Title { get; set; }
public required string Observations { get; set; }
public required string Operations { get; set; }
public string? Notes { get; set; }
}
public sealed class UpdateUserDailyReportRequest
{
public required int Id { get; set; }
public required string Title { get; set; }
public required string Observations { get; set; }
public required string Operations { get; set; }
public string? Notes { get; set; }
}
public sealed class UserDailyReportFilter
{
public int? DeviceId { get; set; }
public int? UserId { get; set; }
public string? PersianDate { get; set; }
public int? Year { get; set; }
public int? Month { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
// DTOs برای مدیریت توکن دستگاه
/// <summary>
/// درخواست دریافت فاصله زمانی آپلود
/// </summary>
public sealed class GetUploadIntervalRequest
{
public int? DeviceId { get; set; }
public string? DevicePhoneNumber { get; set; }
}
/// <summary>
/// پاسخ دریافت فاصله زمانی آپلود
/// </summary>
public sealed class GetUploadIntervalResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public int? UploadIntervalMin { get; set; }
}
/// <summary>
/// درخواست دریافت توکن دستگاه
/// </summary>
public sealed class RequestDeviceTokenRequest
{
public required string DevicePhoneNumber { get; set; }
}
/// <summary>
/// پاسخ دریافت توکن دستگاه
/// </summary>
public sealed class RequestDeviceTokenResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? TokenCode { get; set; }
}
/// <summary>
/// درخواست تایید توکن دستگاه
/// </summary>
public sealed class VerifyDeviceTokenRequest
{
public required string DevicePhoneNumber { get; set; }
public required string VerificationCode { get; set; }
}
/// <summary>
/// پاسخ تایید توکن دستگاه
/// </summary>
public sealed class VerifyDeviceTokenResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? EncodedSettings { get; set; }
}

View File

@@ -0,0 +1,42 @@
namespace GreenHome.Application;
/// <summary>
/// Statistics about AI query usage
/// </summary>
public sealed class AIQueryStats
{
/// <summary>
/// Total number of queries made
/// </summary>
public int TotalQueries { get; set; }
/// <summary>
/// Total tokens used across all queries
/// </summary>
public int TotalTokensUsed { get; set; }
/// <summary>
/// Total prompt tokens used
/// </summary>
public int TotalPromptTokens { get; set; }
/// <summary>
/// Total completion tokens used
/// </summary>
public int TotalCompletionTokens { get; set; }
/// <summary>
/// Average response time in milliseconds
/// </summary>
public double AverageResponseTimeMs { get; set; }
/// <summary>
/// Number of queries made today
/// </summary>
public int TodayQueries { get; set; }
/// <summary>
/// Total tokens used today
/// </summary>
public int TodayTokens { get; set; }
}

View File

@@ -0,0 +1,29 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
public sealed class AlertConditionDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public Domain.AlertNotificationType NotificationType { get; set; }
public Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public List<AlertRuleDto> Rules { get; set; } = new();
}
public sealed class AlertRuleDto
{
public int Id { get; set; }
public int AlertConditionId { get; set; }
public Domain.SensorType SensorType { get; set; }
public Domain.ComparisonType ComparisonType { get; set; }
public decimal Value1 { get; set; }
public decimal? Value2 { get; set; }
public int Order { get; set; }
}

View File

@@ -0,0 +1,22 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
public sealed class AlertLogDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public int? AlertConditionId { get; set; }
public Domain.AlertType AlertType { get; set; }
public Domain.AlertNotificationType NotificationType { get; set; }
public string Message { get; set; } = string.Empty;
public Domain.AlertStatus Status { get; set; }
public string? ErrorMessage { get; set; }
public string PhoneNumber { get; set; } = string.Empty;
public DateTime SentAt { get; set; }
public long ProcessingTimeMs { get; set; }
}

View File

@@ -0,0 +1,15 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
public sealed class AlertLogFilter
{
public int? DeviceId { get; set; }
public int? UserId { get; set; }
public Domain.AlertType? AlertType { get; set; }
public Domain.AlertStatus? Status { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@@ -0,0 +1,34 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
public sealed class CreateAlertConditionRequest
{
public required int DeviceId { get; set; }
public required Domain.AlertNotificationType NotificationType { get; set; }
public required Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public required List<CreateAlertRuleRequest> Rules { get; set; }
}
public sealed class CreateAlertRuleRequest
{
public required Domain.SensorType SensorType { get; set; }
public required Domain.ComparisonType ComparisonType { get; set; }
public required decimal Value1 { get; set; }
public decimal? Value2 { get; set; }
public int Order { get; set; }
}
public sealed class UpdateAlertConditionRequest
{
public required int Id { get; set; }
public required Domain.AlertNotificationType NotificationType { get; set; }
public required Domain.AlertTimeType TimeType { get; set; }
public int CallCooldownMinutes { get; set; } = 60;
public int SmsCooldownMinutes { get; set; } = 15;
public bool IsEnabled { get; set; } = true;
public required List<CreateAlertRuleRequest> Rules { get; set; }
}

View File

@@ -0,0 +1,23 @@
namespace GreenHome.Application;
/// <summary>
/// DTO for relay alert conditions (notificationType > 0)
/// </summary>
public sealed class RelayConditionDto
{
public int Enabled { get; set; }
public int TimeType { get; set; }
public string Relay { get; set; } = string.Empty;
public List<RelayRuleDto> Rules { get; set; } = new();
}
/// <summary>
/// DTO for relay condition rules
/// </summary>
public sealed class RelayRuleDto
{
public string Sensor { get; set; } = string.Empty;
public string Op { get; set; } = string.Empty;
public decimal V1 { get; set; }
public decimal? V2 { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace GreenHome.Application;
public sealed class SendCodeRequest
{
public required string Mobile { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace GreenHome.Application;
public sealed class SendCodeResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public int ResendAfterSeconds { get; set; } = 120;
}

View File

@@ -0,0 +1,12 @@
using Domain = GreenHome.Domain;
namespace GreenHome.Application;
public sealed class UserDto
{
public int Id { get; set; }
public string Mobile { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Family { get; set; } = string.Empty;
public Domain.UserRole Role { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace GreenHome.Application;
public sealed class VerifyCodeRequest
{
public required string Mobile { get; set; }
public required string Code { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace GreenHome.Application;
public sealed class VerifyCodeResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? Token { get; set; }
public UserDto? User { get; set; }
}

View File

@@ -77,4 +77,3 @@ public sealed class CompleteChecklistItemRequest
public bool IsChecked { get; set; } public bool IsChecked { get; set; }
public string? Note { get; set; } public string? Note { get; set; }
} }

View File

@@ -0,0 +1,16 @@
namespace GreenHome.Application;
/// <summary>
/// Result of alert checking, used by TelemetryController.Create to build the line-protocol response
/// </summary>
public sealed class AlertCheckResult
{
/// <summary>List of alert messages (condition alerts + power status)</summary>
public List<string> Messages { get; set; } = new();
/// <summary>Comma-separated recipient phone numbers</summary>
public string? Recipients { get; set; }
/// <summary>Power status: "connected", "disconnected", or null if unchanged</summary>
public string? PowerStatus { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace GreenHome.Application;
public sealed class PagedResult<T>
{
public required IReadOnlyList<T> Items { get; init; }
public required int TotalCount { get; init; }
public required int Page { get; init; }
public required int PageSize { get; init; }
}

View File

@@ -0,0 +1,21 @@
namespace GreenHome.Application;
public sealed class DailyReportRequest
{
public required int DeviceId { get; set; }
public required string PersianDate { get; set; } // yyyy/MM/dd
}
public sealed class WeeklyAnalysisRequest
{
public required int DeviceId { get; set; }
public required string StartDate { get; set; } // yyyy/MM/dd
public required string EndDate { get; set; } // yyyy/MM/dd
}
public sealed class MonthlyAnalysisRequest
{
public required int DeviceId { get; set; }
public required int Year { get; set; }
public required int Month { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace GreenHome.Application;
public sealed class DailyReportResponse
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string PersianDate { get; set; } = string.Empty;
public string Analysis { get; set; } = string.Empty;
public int RecordCount { get; set; }
public int SampledRecordCount { get; set; }
public int TotalTokens { get; set; }
public DateTime CreatedAt { get; set; }
public bool FromCache { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace GreenHome.Application;
public sealed class DeviceDto
{
public int Id { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserFamily { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string NeshanLocation { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,9 @@
namespace GreenHome.Application;
public sealed class DeviceFilter
{
public int? UserId { get; set; }
public string? Search { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
}

View File

@@ -0,0 +1,30 @@
namespace GreenHome.Application;
public sealed class DeviceSettingsDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public string Province { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public decimal? Latitude { get; set; }
public decimal? Longitude { get; set; }
public string ProductType { get; set; } = string.Empty;
public int MinimumSmsIntervalMinutes { get; set; } = 15;
public int MinimumCallIntervalMinutes { get; set; } = 60;
public decimal? AreaSquareMeters { get; set; }
public int UploadIntervalMin { get; set; } = 5;
public string DevicePhoneNumber { get; set; } = string.Empty;
public Domain.SimCardType? SimCardType { get; set; }
public int RelayCount { get; set; } = 4;
public string SmsRecievers { get; set; } = string.Empty;
public string? TokenCode { get; set; }
public string? VerificationCode { get; set; }
public DateTime? TokenExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -43,4 +43,3 @@ public sealed class DevicePostFilter
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20; public int PageSize { get; set; } = 20;
} }

View File

@@ -0,0 +1,20 @@
namespace GreenHome.Application;
/// <summary>
/// درخواست دریافت فاصله زمانی آپلود
/// </summary>
public sealed class GetUploadIntervalRequest
{
public int? DeviceId { get; set; }
public string? DevicePhoneNumber { get; set; }
}
/// <summary>
/// پاسخ دریافت فاصله زمانی آپلود
/// </summary>
public sealed class GetUploadIntervalResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public int? UploadIntervalMin { get; set; }
}

View File

@@ -0,0 +1,19 @@
namespace GreenHome.Application;
/// <summary>
/// درخواست دریافت توکن دستگاه
/// </summary>
public sealed class RequestDeviceTokenRequest
{
public required string DevicePhoneNumber { get; set; }
}
/// <summary>
/// پاسخ دریافت توکن دستگاه
/// </summary>
public sealed class RequestDeviceTokenResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? TokenCode { get; set; }
}

View File

@@ -0,0 +1,20 @@
namespace GreenHome.Application;
/// <summary>
/// درخواست تایید توکن دستگاه
/// </summary>
public sealed class VerifyDeviceTokenRequest
{
public required string DevicePhoneNumber { get; set; }
public required string VerificationCode { get; set; }
}
/// <summary>
/// پاسخ تایید توکن دستگاه
/// </summary>
public sealed class VerifyDeviceTokenResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? EncodedSettings { get; set; }
}

View File

@@ -1,10 +1,5 @@
namespace GreenHome.Application; namespace GreenHome.Application;
public interface IMonthlyReportService
{
Task<MonthlyReportDto> GetMonthlyReportAsync(int deviceId, int year, int month, CancellationToken cancellationToken);
}
public sealed class MonthlyReportDto public sealed class MonthlyReportDto
{ {
public int DeviceId { get; set; } public int DeviceId { get; set; }
@@ -42,4 +37,3 @@ public sealed class MonthlyReportDto
public DateTime GeneratedAt { get; set; } public DateTime GeneratedAt { get; set; }
} }

View File

@@ -0,0 +1,26 @@
namespace GreenHome.Application;
public sealed class PowerOutageLogDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public byte PreviousPower { get; set; }
public byte CurrentPower { get; set; }
public string EventType { get; set; } = string.Empty; // "Connected" or "Disconnected"
public string EventTypePersian { get; set; } = string.Empty; // "وصل" or "قطع"
public DateTime TimestampUtc { get; set; }
public DateTime ServerTimestampUtc { get; set; }
public string PersianDate { get; set; } = string.Empty;
}
public sealed class PowerOutageFilter
{
public int? EventType { get; set; } // 1 = Connected, 2 = Disconnected
public DateTime? StartDateUtc { get; set; }
public DateTime? EndDateUtc { get; set; }
public int? PersianYear { get; set; }
public int? PersianMonth { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@@ -0,0 +1,24 @@
namespace GreenHome.Application;
/// <summary>
/// خلاصه قطعی‌های برق یک دستگاه در یک بازه زمانی
/// </summary>
public sealed class PowerOutageSummaryDto
{
public int DeviceId { get; set; }
public int TotalOutageCount { get; set; }
public long TotalDowntimeSeconds { get; set; }
public DateTime? StartDateUtc { get; set; }
public DateTime? EndDateUtc { get; set; }
public List<PowerOutagePairDto> Outages { get; set; } = new();
}
/// <summary>
/// یک جفت قطع و وصل برق (مدت زمان قطعی)
/// </summary>
public sealed class PowerOutagePairDto
{
public DateTime DisconnectedAt { get; set; }
public DateTime? ConnectedAt { get; set; }
public long? DurationSeconds { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace GreenHome.Application;
public sealed class DayCount
{
public string PersianDate { get; set; } = string.Empty;
public int Count { get; set; }
}

View File

@@ -0,0 +1,23 @@
namespace GreenHome.Application;
public sealed class TelemetryDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string? DeviceName { get; set; }
public DateTime? TimestampUtc { get; set; }
public decimal TemperatureC { get; set; }
public decimal HumidityPercent { get; set; }
public decimal SoilPercent { get; set; }
public int GasPPM { get; set; }
public decimal? Voltage { get; set; }
public decimal? BatteryVoltage { get; set; }
public decimal? BatteryPercent { get; set; }
public byte? Power { get; set; }
public byte? OldPower { get; set; }
public decimal Lux { get; set; }
public int? PersianYear { get; set; }
public int? PersianMonth { get; set; }
public string PersianDate { get; set; } = string.Empty;
public DateTime? ServerTimestampUtc { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace GreenHome.Application;
public sealed class TelemetryFilter
{
public int? DeviceId { get; set; }
public DateTime? StartDateUtc { get; set; }
public DateTime? EndDateUtc { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@@ -0,0 +1,15 @@
namespace GreenHome.Application;
public sealed class TelemetryMinMax
{
public decimal MinTemperatureC { get; set; }
public decimal MaxTemperatureC { get; set; }
public decimal MinHumidityPercent { get; set; }
public decimal MaxHumidityPercent { get; set; }
public decimal MinSoilPercent { get; set; }
public decimal MaxSoilPercent { get; set; }
public int MinGasPPM { get; set; }
public int MaxGasPPM { get; set; }
public decimal MinLux { get; set; }
public decimal MaxLux { get; set; }
}

View File

@@ -0,0 +1,32 @@
namespace GreenHome.Application;
public sealed class CreateUserDailyReportRequest
{
public required int DeviceId { get; set; }
public required int UserId { get; set; }
public required string PersianDate { get; set; }
public required string Title { get; set; }
public required string Observations { get; set; }
public required string Operations { get; set; }
public string? Notes { get; set; }
}
public sealed class UpdateUserDailyReportRequest
{
public required int Id { get; set; }
public required string Title { get; set; }
public required string Observations { get; set; }
public required string Operations { get; set; }
public string? Notes { get; set; }
}
public sealed class UserDailyReportFilter
{
public int? DeviceId { get; set; }
public int? UserId { get; set; }
public string? PersianDate { get; set; }
public int? Year { get; set; }
public int? Month { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}

View File

@@ -0,0 +1,30 @@
namespace GreenHome.Application;
public sealed class UserDailyReportDto
{
public int Id { get; set; }
public int DeviceId { get; set; }
public string DeviceName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string UserFamily { get; set; } = string.Empty;
public string PersianDate { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Observations { get; set; } = string.Empty;
public string Operations { get; set; } = string.Empty;
public string? Notes { get; set; }
public List<ReportImageDto> Images { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public sealed class ReportImageDto
{
public int Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public string ContentType { get; set; } = string.Empty;
public long FileSize { get; set; }
public string? Description { get; set; }
public DateTime UploadedAt { get; set; }
}

View File

@@ -33,27 +33,7 @@ public interface IAIQueryService
Task<int> GetUserTotalTokensAsync(int userId, CancellationToken cancellationToken = default); Task<int> GetUserTotalTokensAsync(int userId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Gets recent queries (all) /// Gets AI query statistics (totals, averages, today's usage)
/// </summary>
Task<List<AIQuery>> GetRecentQueriesAsync(int take = 20, CancellationToken cancellationToken = default);
/// <summary>
/// Gets query statistics
/// </summary> /// </summary>
Task<AIQueryStats> GetStatsAsync(CancellationToken cancellationToken = default); Task<AIQueryStats> GetStatsAsync(CancellationToken cancellationToken = default);
} }
/// <summary>
/// Statistics about AI queries
/// </summary>
public class AIQueryStats
{
public int TotalQueries { get; set; }
public int TotalTokensUsed { get; set; }
public int TotalPromptTokens { get; set; }
public int TotalCompletionTokens { get; set; }
public double AverageResponseTimeMs { get; set; }
public int TodayQueries { get; set; }
public int TodayTokens { get; set; }
}

View File

@@ -22,4 +22,3 @@ public interface IAlertConditionService
Task<bool> HasUpdatedConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken); Task<bool> HasUpdatedConditionsAsync(int deviceId, DateTime? lastUpdateDate, CancellationToken cancellationToken);
} }

View File

@@ -6,4 +6,3 @@ public interface IAlertLogService
Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken); Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken);
Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken); Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken);
} }

View File

@@ -2,7 +2,6 @@ namespace GreenHome.Application;
public interface IAlertService public interface IAlertService
{ {
Task<string?> CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken); Task<AlertCheckResult?> CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken); Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken);
} }

View File

@@ -6,4 +6,3 @@ public interface IAuthService
Task<VerifyCodeResponse> VerifyCodeAsync(VerifyCodeRequest request, CancellationToken cancellationToken); Task<VerifyCodeResponse> VerifyCodeAsync(VerifyCodeRequest request, CancellationToken cancellationToken);
Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken); Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken);
} }

View File

@@ -9,4 +9,3 @@ public interface IChecklistService
Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(int checklistId, CancellationToken cancellationToken); Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(int checklistId, CancellationToken cancellationToken);
Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken); Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken);
} }

View File

@@ -14,4 +14,3 @@ public interface IDailyReportService
Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken); Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken);
} }

View File

@@ -11,4 +11,3 @@ public interface IDevicePostService
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken); Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
Task<bool> CanUserAccessDeviceAsync(int userId, int deviceId, CancellationToken cancellationToken); Task<bool> CanUserAccessDeviceAsync(int userId, int deviceId, CancellationToken cancellationToken);
} }

View File

@@ -1,6 +1,5 @@
namespace GreenHome.Application; namespace GreenHome.Application;
// Services
public interface IDeviceService public interface IDeviceService
{ {
Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken); Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken);

View File

@@ -1,4 +1,4 @@
namespace GreenHome.Application; namespace GreenHome.Application;
public interface IDeviceSettingsService public interface IDeviceSettingsService
{ {

View File

@@ -20,4 +20,3 @@ public interface IDeviceTokenService
/// </summary> /// </summary>
Task<VerifyDeviceTokenResponse> VerifyDeviceTokenAsync(VerifyDeviceTokenRequest request, CancellationToken cancellationToken); Task<VerifyDeviceTokenResponse> VerifyDeviceTokenAsync(VerifyDeviceTokenRequest request, CancellationToken cancellationToken);
} }

View File

@@ -0,0 +1,6 @@
namespace GreenHome.Application;
public interface IMonthlyReportService
{
Task<MonthlyReportDto> GetMonthlyReportAsync(int deviceId, int year, int month, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,23 @@
namespace GreenHome.Application;
/// <summary>
/// سرویس لاگ قطع و وصل برق دستگاه
/// </summary>
public interface IPowerOutageLogService
{
/// <summary>
/// ثبت یک رویداد قطع یا وصل برق
/// </summary>
Task LogPowerChangeAsync(int deviceId, byte previousPower, byte currentPower, DateTime timestampUtc, CancellationToken cancellationToken);
/// <summary>
/// دریافت لیست رویدادهای برق یک دستگاه
/// </summary>
Task<PagedResult<PowerOutageLogDto>> ListAsync(int deviceId, PowerOutageFilter filter, CancellationToken cancellationToken);
/// <summary>
/// مهاجرت داده‌های قدیمی از جدول Telemetry به PowerOutageLogs
/// تشخیص تغییرات Power در رکوردهای متوالی هر دستگاه
/// </summary>
Task<int> MigrateFromTelemetryAsync(int? deviceId, CancellationToken cancellationToken);
}

View File

@@ -11,4 +11,3 @@ public interface ISunCalculatorService
/// <returns>true اگر روز باشد، false اگر شب باشد</returns> /// <returns>true اگر روز باشد، false اگر شب باشد</returns>
bool IsDaytime(DateTime dateTime, decimal latitude, decimal longitude); bool IsDaytime(DateTime dateTime, decimal latitude, decimal longitude);
} }

View File

@@ -1,4 +1,4 @@
namespace GreenHome.Application; namespace GreenHome.Application;
public interface ITelemetryService public interface ITelemetryService
{ {

View File

@@ -10,4 +10,3 @@ public interface IUserDailyReportService
Task<int> AddImageToReportAsync(int reportId, string fileName, string filePath, string contentType, long fileSize, string? description, CancellationToken cancellationToken); Task<int> AddImageToReportAsync(int reportId, string fileName, string filePath, string contentType, long fileSize, string? description, CancellationToken cancellationToken);
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken); Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
} }

View File

@@ -1,8 +1,7 @@
using FluentValidation; using FluentValidation;
namespace GreenHome.Application; namespace GreenHome.Application;
// Validation
public sealed class DeviceDtoValidator : AbstractValidator<DeviceDto> public sealed class DeviceDtoValidator : AbstractValidator<DeviceDto>
{ {
public DeviceDtoValidator() public DeviceDtoValidator()

View File

@@ -0,0 +1,60 @@
namespace GreenHome.Domain;
/// <summary>
/// لاگ قطع و وصل برق دستگاه
/// </summary>
public sealed class PowerOutageLog
{
public int Id { get; set; }
/// <summary>
/// شناسه دستگاه
/// </summary>
public int DeviceId { get; set; }
public Device Device { get; set; } = null!;
/// <summary>
/// وضعیت قبلی برق (0 = قطع, 1 = وصل)
/// </summary>
public byte PreviousPower { get; set; }
/// <summary>
/// وضعیت جدید برق (0 = قطع, 1 = وصل)
/// </summary>
public byte CurrentPower { get; set; }
/// <summary>
/// نوع رویداد: قطع یا وصل
/// </summary>
public PowerEventType EventType { get; set; }
/// <summary>
/// زمان رویداد (از دستگاه)
/// </summary>
public DateTime TimestampUtc { get; set; }
/// <summary>
/// زمان ثبت در سرور
/// </summary>
public DateTime ServerTimestampUtc { get; set; }
public int PersianYear { get; set; }
public int PersianMonth { get; set; }
public string PersianDate { get; set; } = string.Empty; // yyyy/MM/dd
}
/// <summary>
/// نوع رویداد برق
/// </summary>
public enum PowerEventType
{
/// <summary>
/// برق وصل شده
/// </summary>
Connected = 1,
/// <summary>
/// برق قطع شده
/// </summary>
Disconnected = 2
}

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

@@ -0,0 +1,64 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GreenHome.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddPowerOutageLogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PowerOutageLogs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DeviceId = table.Column<int>(type: "int", nullable: false),
PreviousPower = table.Column<byte>(type: "tinyint", nullable: false),
CurrentPower = table.Column<byte>(type: "tinyint", nullable: false),
EventType = table.Column<int>(type: "int", nullable: false),
TimestampUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
ServerTimestampUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
PersianYear = table.Column<int>(type: "int", nullable: false),
PersianMonth = table.Column<int>(type: "int", nullable: false),
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PowerOutageLogs", x => x.Id);
table.ForeignKey(
name: "FK_PowerOutageLogs_Devices_DeviceId",
column: x => x.DeviceId,
principalTable: "Devices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PowerOutageLogs_DeviceId_PersianYear_PersianMonth",
table: "PowerOutageLogs",
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
migrationBuilder.CreateIndex(
name: "IX_PowerOutageLogs_DeviceId_TimestampUtc",
table: "PowerOutageLogs",
columns: new[] { "DeviceId", "TimestampUtc" });
migrationBuilder.CreateIndex(
name: "IX_PowerOutageLogs_EventType",
table: "PowerOutageLogs",
column: "EventType");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PowerOutageLogs");
}
}
}

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);
} }
} }

Some files were not shown because too many files have changed in this diff Show More