feat: add PowerOutageLog entity, service, migration and API endpoints
This commit is contained in:
@@ -8,13 +8,16 @@ namespace GreenHome.Api.Controllers;
|
||||
public class PowerOutageController : ControllerBase
|
||||
{
|
||||
private readonly IAlertService _alertService;
|
||||
private readonly IPowerOutageLogService _powerOutageLogService;
|
||||
private readonly ILogger<PowerOutageController> _logger;
|
||||
|
||||
public PowerOutageController(
|
||||
IAlertService alertService,
|
||||
IPowerOutageLogService powerOutageLogService,
|
||||
ILogger<PowerOutageController> logger)
|
||||
{
|
||||
_alertService = alertService;
|
||||
_powerOutageLogService = powerOutageLogService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -40,9 +43,9 @@ public class PowerOutageController : ControllerBase
|
||||
|
||||
_logger.LogInformation("Power outage alert processed for device {DeviceId}", deviceId);
|
||||
|
||||
return Ok(new {
|
||||
success = true,
|
||||
message = "هشدار قطع برق با موفقیت ارسال شد"
|
||||
return Ok(new {
|
||||
success = true,
|
||||
message = "هشدار قطع برق با موفقیت ارسال شد"
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
@@ -56,5 +59,175 @@ public class PowerOutageController : ControllerBase
|
||||
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}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
src/GreenHome.Domain/PowerOutageLog.cs
Normal file
60
src/GreenHome.Domain/PowerOutageLog.cs
Normal 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
|
||||
}
|
||||
1310
src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260601132156_AddPowerOutageLogs.Designer.cs
generated
Normal file
1310
src/GreenHome.Infrastructure/Data/Migrations/Migrations/20260601132156_AddPowerOutageLogs.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Infrastructure.Data;
|
||||
using GreenHome.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure.Services.PowerOutage;
|
||||
|
||||
public sealed class PowerOutageLogService : IPowerOutageLogService
|
||||
{
|
||||
private readonly GreenHomeDbContext _dbContext;
|
||||
|
||||
public PowerOutageLogService(GreenHomeDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
public async Task LogPowerChangeAsync(int deviceId, byte previousPower, byte currentPower, DateTime timestampUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
var (year, month, day, persianDate) = timestampUtc.ToPersianDate();
|
||||
|
||||
var log = new Domain.PowerOutageLog
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
PreviousPower = previousPower,
|
||||
CurrentPower = currentPower,
|
||||
EventType = currentPower == 1 ? Domain.PowerEventType.Connected : Domain.PowerEventType.Disconnected,
|
||||
TimestampUtc = timestampUtc,
|
||||
ServerTimestampUtc = DateTime.UtcNow,
|
||||
PersianYear = year,
|
||||
PersianMonth = month,
|
||||
PersianDate = persianDate
|
||||
};
|
||||
|
||||
_dbContext.PowerOutageLogs.Add(log);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<PowerOutageLogDto>> ListAsync(int deviceId, PowerOutageFilter filter, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _dbContext.PowerOutageLogs
|
||||
.AsNoTracking()
|
||||
.Where(x => x.DeviceId == deviceId);
|
||||
|
||||
if (filter.EventType.HasValue)
|
||||
{
|
||||
var eventType = (Domain.PowerEventType)filter.EventType.Value;
|
||||
query = query.Where(x => x.EventType == eventType);
|
||||
}
|
||||
|
||||
if (filter.StartDateUtc.HasValue)
|
||||
{
|
||||
var start = filter.StartDateUtc.Value.ToLocalTime();
|
||||
query = query.Where(x => x.ServerTimestampUtc >= start);
|
||||
}
|
||||
|
||||
if (filter.EndDateUtc.HasValue)
|
||||
{
|
||||
var end = filter.EndDateUtc.Value.ToLocalTime();
|
||||
query = query.Where(x => x.ServerTimestampUtc < end);
|
||||
}
|
||||
|
||||
if (filter.PersianYear.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.PersianYear == filter.PersianYear.Value);
|
||||
}
|
||||
|
||||
if (filter.PersianMonth.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.PersianMonth == filter.PersianMonth.Value);
|
||||
}
|
||||
|
||||
if (filter.Page <= 0) filter.Page = 1;
|
||||
if (filter.PageSize <= 0) filter.PageSize = 20;
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var skip = (filter.Page - 1) * filter.PageSize;
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.TimestampUtc)
|
||||
.Skip(skip)
|
||||
.Take(filter.PageSize)
|
||||
.Select(x => new PowerOutageLogDto
|
||||
{
|
||||
Id = x.Id,
|
||||
DeviceId = x.DeviceId,
|
||||
DeviceName = x.Device.DeviceName,
|
||||
PreviousPower = x.PreviousPower,
|
||||
CurrentPower = x.CurrentPower,
|
||||
EventType = x.EventType.ToString(),
|
||||
EventTypePersian = x.EventType == Domain.PowerEventType.Connected ? "وصل" : "قطع",
|
||||
TimestampUtc = x.TimestampUtc,
|
||||
ServerTimestampUtc = x.ServerTimestampUtc,
|
||||
PersianDate = x.PersianDate
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<PowerOutageLogDto>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = total,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> MigrateFromTelemetryAsync(int? deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _dbContext.TelemetryRecords
|
||||
.Where(x => x.Power != null);
|
||||
|
||||
if (deviceId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.DeviceId == deviceId.Value);
|
||||
}
|
||||
|
||||
// Get all relevant telemetry records grouped by device, ordered by timestamp
|
||||
var telemetryGroups = await query
|
||||
.OrderBy(x => x.DeviceId)
|
||||
.ThenBy(x => x.TimestampUtc)
|
||||
.Select(x => new { x.Id, x.DeviceId, x.Power, x.TimestampUtc })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Group by device and detect power changes
|
||||
var groups = telemetryGroups.GroupBy(x => x.DeviceId);
|
||||
var totalInserted = 0;
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
Domain.PowerOutageLog? previousLog = null;
|
||||
|
||||
foreach (var record in group)
|
||||
{
|
||||
if (previousLog != null && previousLog.CurrentPower != record.Power)
|
||||
{
|
||||
// Power status changed - insert a log
|
||||
var (year, month, day, persianDate) = record.TimestampUtc.ToPersianDate();
|
||||
|
||||
var log = new Domain.PowerOutageLog
|
||||
{
|
||||
DeviceId = record.DeviceId,
|
||||
PreviousPower = previousLog.CurrentPower,
|
||||
CurrentPower = record.Power.Value,
|
||||
EventType = record.Power == 1 ? Domain.PowerEventType.Connected : Domain.PowerEventType.Disconnected,
|
||||
TimestampUtc = record.TimestampUtc,
|
||||
ServerTimestampUtc = DateTime.UtcNow,
|
||||
PersianYear = year,
|
||||
PersianMonth = month,
|
||||
PersianDate = persianDate
|
||||
};
|
||||
|
||||
_dbContext.PowerOutageLogs.Add(log);
|
||||
totalInserted++;
|
||||
}
|
||||
|
||||
// Track last known power state for this device
|
||||
previousLog = new Domain.PowerOutageLog
|
||||
{
|
||||
DeviceId = record.DeviceId,
|
||||
CurrentPower = record.Power.Value,
|
||||
TimestampUtc = record.TimestampUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
return totalInserted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user