using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Core.Domain.Entities;
using Hc900Crawler.Infrastructure.Database;
using Hc900Crawler.Infrastructure.Hc900;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Hc900Crawler.Web.Controllers;
// ── Gateway 상태 / 제어 ──────────────────────────────────────────────────────
[ApiController]
[Route("api/gateway")]
public class GatewayController : ControllerBase
{
private readonly ControllerGrpcClientPool _clientPool;
private readonly Hc900WriteService _write;
private readonly Hc900RealtimeService _realtime;
public GatewayController(ControllerGrpcClientPool clientPool, Hc900WriteService write, Hc900RealtimeService realtime)
{
_clientPool = clientPool;
_write = write;
_realtime = realtime;
}
/// gRPC Gateway 헬스체크
[HttpGet("health")]
public async Task Health([FromQuery] string? controllerId = null)
{
try
{
var id = controllerId ?? _clientPool.EnabledControllerIds.FirstOrDefault() ?? "HC1";
var client = _clientPool.GetClient(id);
var h = await client.HealthCheckAsync(new Hc900.Gateway.HealthCheckRequest());
return Ok(new
{
controllerId = id,
status = h.Status.ToString(),
controllerIp = h.ControllerIp,
activeTags = h.ActiveTags,
pollCount = h.PollCount,
lastPollMs = h.LastPollMs,
uptimeSec = h.UptimeSec
});
}
catch (Exception ex)
{
return Ok(new { status = "NOT_SERVING", error = ex.Message });
}
}
/// 실시간 수집 서비스 상태
[HttpGet("status")]
public IActionResult Status() => Ok(new
{
isConnected = _realtime.IsConnected,
pollCount = _realtime.PollCount,
lastPollAt = _realtime.LastPollAt,
controllers = _realtime.ControllerConnected,
});
/// 레지스터 태그 목록 조회
[HttpGet("tags")]
public async Task ListTags([FromQuery] string? controllerId = null, [FromQuery] string filter = "", [FromQuery] int limit = 100)
{
var id = controllerId ?? _clientPool.EnabledControllerIds.FirstOrDefault() ?? "HC1";
var client = _clientPool.GetClient(id);
var resp = await client.ListTagsAsync(new Hc900.Gateway.ListTagsRequest { Filter = filter, Limit = limit });
return Ok(resp.Tags.Select(t => new
{
tagName = t.TagName,
address = t.Address,
count = t.Count,
type = t.Type,
access = t.Access,
description = t.Description,
eu = t.Eu
}));
}
/// 태그 쓰기
[HttpPost("write")]
public async Task Write([FromBody] WriteTagDto dto)
{
var (success, error) = await _write.WriteTagAsync(dto.ControllerId, dto.TagName, dto.Value);
return Ok(new { success, error });
}
}
public class WriteTagDto
{
public string ControllerId { get; set; } = "HC1";
public string TagName { get; set; } = "";
public double Value { get; set; }
}
// ── Realtime Points ───────────────────────────────────────────────────────────
[ApiController]
[Route("api/realtime")]
public class RealtimeController : ControllerBase
{
private readonly IExperionDbService _db;
public RealtimeController(IExperionDbService db) => _db = db;
/// 전체 realtime 포인트 조회 (선택적 controllerId 필터)
[HttpGet("points")]
public async Task GetPoints([FromQuery] string? controllerId = null)
{
var points = await _db.GetRealtimePointsAsync();
if (!string.IsNullOrEmpty(controllerId))
points = points.Where(p => p.ControllerId == controllerId);
return Ok(points);
}
[HttpDelete("points/{id}")]
public async Task DeletePoint(int id, [FromQuery] bool purgeHistory = false)
{
var result = await _db.DeleteRealtimePointAsync(id, purgeHistory);
return Ok(result);
}
}
// ── History ───────────────────────────────────────────────────────────────────
[ApiController]
[Route("api/history")]
public class HistoryController : ControllerBase
{
private readonly IExperionDbService _db;
public HistoryController(IExperionDbService db) => _db = db;
[HttpPost("query")]
public async Task Query([FromBody] HistoryQueryDto dto)
{
var result = await _db.QueryHistoryAsync(dto.TagNames, dto.From, dto.To, dto.Limit);
return Ok(result);
}
[HttpPost("query-interval")]
public async Task QueryInterval([FromBody] Hc900Crawler.Core.Application.Interfaces.HistoryIntervalQueryRequest req)
{
var result = await _db.QueryHistoryWithIntervalAsync(req);
return Ok(result);
}
[HttpGet("tags")]
public async Task GetTagNames()
{
var tags = await _db.GetTagNamesAsync();
return Ok(tags);
}
}
public class HistoryQueryDto
{
public string? ControllerId { get; set; }
public List TagNames { get; set; } = new();
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public int Limit { get; set; } = 1000;
}
// ── Tag Metadata ──────────────────────────────────────────────────────────────
[ApiController]
[Route("api/metadata")]
public class MetadataController : ControllerBase
{
private readonly IExperionDbService _db;
public MetadataController(IExperionDbService db) => _db = db;
[HttpGet]
public async Task Get([FromQuery] string? baseTag = null)
{
using var scope = HttpContext.RequestServices.CreateScope();
var ctx = scope.ServiceProvider.GetRequiredService();
var q = ctx.TagMetadata.AsQueryable();
if (!string.IsNullOrEmpty(baseTag))
q = q.Where(x => x.BaseTag == baseTag);
return Ok(await q.ToListAsync());
}
}
// ── Event History ─────────────────────────────────────────────────────────────
[ApiController]
[Route("api/events")]
public class EventHistoryController : ControllerBase
{
private readonly IExperionDbService _db;
public EventHistoryController(IExperionDbService db) => _db = db;
[HttpPost("query")]
public async Task Query([FromBody] EventQueryDto dto)
{
var result = await _db.QueryEventHistoryAsync(
dto.TagName, dto.Area, dto.SubArea, dto.EventType,
dto.From ?? DateTime.UtcNow.AddDays(-1),
dto.To ?? DateTime.UtcNow,
dto.Limit);
return Ok(result);
}
}
public class EventQueryDto
{
public string? ControllerId { get; set; }
public string? TagName { get; set; }
public string? Area { get; set; }
public string? SubArea { get; set; }
public string? EventType { get; set; }
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public int Limit { get; set; } = 500;
}
// ── HC900 Tag Manager ─────────────────────────────────────────────────────────
[ApiController]
[Route("api/hc900/tags")]
public class Hc900TagManagerController : ControllerBase
{
private readonly Hc900DbContext _ctx;
public Hc900TagManagerController(Hc900DbContext ctx) => _ctx = ctx;
[HttpGet]
public async Task Get(
[FromQuery] string? controllerId = null,
[FromQuery] string? paramType = null,
[FromQuery] int? loopNo = null,
[FromQuery] bool? active = null,
[FromQuery] string? search = null)
{
var q = _ctx.Hc900MapEntries.AsQueryable();
if (!string.IsNullOrEmpty(controllerId)) q = q.Where(x => x.ControllerId == controllerId);
if (active.HasValue) q = q.Where(x => x.IsActive == active.Value);
if (!string.IsNullOrEmpty(paramType)) q = q.Where(x => x.ParamType == paramType);
if (loopNo.HasValue) q = q.Where(x => x.LoopNo == loopNo.Value);
if (!string.IsNullOrEmpty(search))
q = q.Where(x => x.TagName.Contains(search) || x.Hc900Tag.Contains(search));
var entries = await q.ToListAsync();
entries.Sort((a, b) =>
{
var ca = char.ToUpper(a.TagName[0]);
var cb = char.ToUpper(b.TagName[0]);
if (ca != cb) return ca.CompareTo(cb);
var dotA = a.TagName.LastIndexOf('.');
var dotB = b.TagName.LastIndexOf('.');
var baseA = dotA >= 0 ? a.TagName[..dotA] : a.TagName;
var baseB = dotB >= 0 ? b.TagName[..dotB] : b.TagName;
var propA = dotA >= 0 ? a.TagName[(dotA + 1)..] : "";
var propB = dotB >= 0 ? b.TagName[(dotB + 1)..] : "";
var cmp = string.Compare(baseA, baseB, StringComparison.OrdinalIgnoreCase);
if (cmp != 0) return cmp;
return string.Compare(propA, propB, StringComparison.OrdinalIgnoreCase);
});
var tagNames = entries.Select(e => e.TagName).ToList();
var liveValues = await _ctx.RealtimePoints
.Where(r => tagNames.Contains(r.TagName))
.ToDictionaryAsync(r => r.TagName, r => new { r.LiveValue, r.Timestamp });
var result = entries.Select(e =>
{
liveValues.TryGetValue(e.TagName, out var live);
return new
{
e.Id, e.TagName, e.Hc900Tag, e.ControllerId,
ModbusAddrHex = $"0x{e.ModbusAddr:X4}",
e.ModbusAddr, e.DataType, e.Access,
e.LoopNo, e.ParamType, e.IsActive,
LiveValue = live?.LiveValue,
Timestamp = live?.Timestamp,
};
});
return Ok(result);
}
[HttpPut("{id}/active")]
public async Task SetActive(int id, [FromBody] bool active)
{
var entry = await _ctx.Hc900MapEntries.FindAsync(id);
if (entry == null) return NotFound();
entry.IsActive = active;
await _ctx.SaveChangesAsync();
return Ok(new { entry.Id, entry.TagName, entry.IsActive });
}
[HttpPost("bulk-active")]
public async Task BulkSetActive([FromBody] BulkActiveDto dto)
{
var q = _ctx.Hc900MapEntries.AsQueryable();
if (!string.IsNullOrEmpty(dto.ParamType)) q = q.Where(x => x.ParamType == dto.ParamType);
if (dto.LoopNo.HasValue) q = q.Where(x => x.LoopNo == dto.LoopNo.Value);
if (dto.Ids?.Any() == true) q = q.Where(x => dto.Ids.Contains(x.Id));
var count = await q.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, dto.Active));
return Ok(new { updated = count, active = dto.Active });
}
[HttpGet("param-types")]
public async Task GetParamTypes()
{
var types = await _ctx.Hc900MapEntries
.Where(x => x.ParamType != null)
.Select(x => x.ParamType!)
.Distinct()
.OrderBy(x => x)
.ToListAsync();
return Ok(types);
}
[HttpPut("{id}/controller")]
public async Task SetController(int id, [FromBody] string controllerId)
{
var entry = await _ctx.Hc900MapEntries.FindAsync(id);
if (entry == null) return NotFound();
entry.ControllerId = controllerId;
await _ctx.SaveChangesAsync();
return Ok(new { entry.Id, entry.ControllerId });
}
[HttpGet("controller-ids")]
public async Task GetControllerIds()
{
var ids = await _ctx.Hc900MapEntries
.Select(x => x.ControllerId)
.Distinct()
.OrderBy(x => x)
.ToListAsync();
return Ok(ids);
}
[HttpGet("summary")]
public async Task GetSummary()
{
var total = await _ctx.Hc900MapEntries.CountAsync();
var active = await _ctx.Hc900MapEntries.CountAsync(x => x.IsActive);
var byType = await _ctx.Hc900MapEntries
.GroupBy(x => x.ParamType)
.Select(g => new { paramType = g.Key, total = g.Count(), active = g.Count(x => x.IsActive) })
.OrderBy(x => x.paramType)
.ToListAsync();
var liveCount = await _ctx.RealtimePoints.CountAsync();
return Ok(new { total, active, inactive = total - active, liveCount, byType });
}
}
public class BulkActiveDto
{
public bool Active { get; set; }
public string? ParamType { get; set; }
public int? LoopNo { get; set; }
public List? Ids { get; set; }
}
// ── Sub-Area ──────────────────────────────────────────────────────────────────
[ApiController]
[Route("api/subarea")]
public class SubAreaController : ControllerBase
{
private readonly IExperionDbService _db;
public SubAreaController(IExperionDbService db) => _db = db;
[HttpGet("{area}")]
public async Task Get(string area, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
{
var (tags, total) = await _db.GetSubAreaListByAreaAsync(area, page, pageSize);
return Ok(new { total, tags });
}
[HttpPut("{baseTag}")]
public async Task Update(string baseTag, [FromBody] string? subArea)
{
var ok = await _db.UpdateSubAreaAsync(baseTag, subArea);
return Ok(new { success = ok });
}
[HttpPost("seed")]
public async Task Seed([FromQuery] bool dryRun = true)
{
var result = await _db.SeedSubAreaAsync(dryRun);
return Ok(result);
}
}