- FeedforwardSupervisor: PvTag() ToUpperInvariant + empty FeedTag 가드 - FeedforwardConfigStore: 모든 ToLowerInvariant() 제거 - FeedRampAdvisorService: ToLowerInvariant 제거 + StringComparison.OrdinalIgnoreCase - SimOverrideStore: ToLowerInvariant 제거 - Hc900RealtimeService: HealthCheck SERVING 판정, mapping 없는 태그 대소문자 유지 - PidExtractorService: ToLowerInvariant → OrdinalIgnoreCase 비교 - Hc900Entities: 주석 업데이트 (대문자 표준) - load_state_labels.py: 소문자 변환 금지, controller_id 파라미터 추가 - Hc900Controllers: 대소문자 무시 정렬 - write.js: .MODE → AutoManState/RemLocSPState/SP_SelectState/TuneSetState - setup.js/html: 중복 함수 제거, C5 컨트롤러 placeholder
400 lines
14 KiB
C#
400 lines
14 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>gRPC Gateway 헬스체크</summary>
|
|
[HttpGet("health")]
|
|
public async Task<IActionResult> 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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>실시간 수집 서비스 상태</summary>
|
|
[HttpGet("status")]
|
|
public IActionResult Status() => Ok(new
|
|
{
|
|
isConnected = _realtime.IsConnected,
|
|
pollCount = _realtime.PollCount,
|
|
lastPollAt = _realtime.LastPollAt,
|
|
controllers = _realtime.ControllerConnected,
|
|
});
|
|
|
|
/// <summary>레지스터 태그 목록 조회</summary>
|
|
[HttpGet("tags")]
|
|
public async Task<IActionResult> 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
|
|
}));
|
|
}
|
|
|
|
/// <summary>태그 쓰기</summary>
|
|
[HttpPost("write")]
|
|
public async Task<IActionResult> 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;
|
|
|
|
/// <summary>전체 realtime 포인트 조회 (선택적 controllerId 필터)</summary>
|
|
[HttpGet("points")]
|
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> QueryInterval([FromBody] Hc900Crawler.Core.Application.Interfaces.HistoryIntervalQueryRequest req)
|
|
{
|
|
var result = await _db.QueryHistoryWithIntervalAsync(req);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("tags")]
|
|
public async Task<IActionResult> GetTagNames()
|
|
{
|
|
var tags = await _db.GetTagNamesAsync();
|
|
return Ok(tags);
|
|
}
|
|
}
|
|
|
|
public class HistoryQueryDto
|
|
{
|
|
public string? ControllerId { get; set; }
|
|
public List<string> 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<IActionResult> Get([FromQuery] string? baseTag = null)
|
|
{
|
|
using var scope = HttpContext.RequestServices.CreateScope();
|
|
var ctx = scope.ServiceProvider.GetRequiredService<Hc900DbContext>();
|
|
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetControllerIds()
|
|
{
|
|
var ids = await _ctx.Hc900MapEntries
|
|
.Select(x => x.ControllerId)
|
|
.Distinct()
|
|
.OrderBy(x => x)
|
|
.ToListAsync();
|
|
return Ok(ids);
|
|
}
|
|
|
|
[HttpGet("summary")]
|
|
public async Task<IActionResult> 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<int>? 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<IActionResult> 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<IActionResult> Update(string baseTag, [FromBody] string? subArea)
|
|
{
|
|
var ok = await _db.UpdateSubAreaAsync(baseTag, subArea);
|
|
return Ok(new { success = ok });
|
|
}
|
|
|
|
[HttpPost("seed")]
|
|
public async Task<IActionResult> Seed([FromQuery] bool dryRun = true)
|
|
{
|
|
var result = await _db.SeedSubAreaAsync(dryRun);
|
|
return Ok(result);
|
|
}
|
|
}
|