using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
///
/// Per-instance detail view for FOCAS driver rows. Loads the latest
/// row for the requested DriverInstanceId (most-recent
/// draft wins when multiple rows exist across generations), parses the schemaless
/// DriverConfig JSON into , and joins the
/// per-device rows so the Admin page can render host
/// state + consecutive-failure counters next to each configured device.
///
public sealed class FocasDriverDetailService(OtOpcUaConfigDbContext db)
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
public async Task GetAsync(string driverInstanceId, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(driverInstanceId)) return null;
var instance = await db.DriverInstances.AsNoTracking()
.Where(d => d.DriverInstanceId == driverInstanceId
&& d.DriverType.ToLower() == "focas")
.OrderByDescending(d => d.GenerationId)
.FirstOrDefaultAsync(ct);
if (instance is null) return null;
FocasDriverConfigView? config = null;
string? parseError = null;
try { config = JsonSerializer.Deserialize(instance.DriverConfig, JsonOpts); }
catch (JsonException ex) { parseError = ex.Message; }
var hostStatuses = await (from s in db.DriverHostStatuses.AsNoTracking()
where s.DriverInstanceId == driverInstanceId
join r in db.DriverInstanceResilienceStatuses.AsNoTracking()
on new { s.DriverInstanceId, s.HostName }
equals new { r.DriverInstanceId, r.HostName } into rj
from r in rj.DefaultIfEmpty()
orderby s.HostName
select new FocasHostStatusRow(
s.NodeId,
s.HostName,
s.State.ToString(),
s.StateChangedUtc,
s.LastSeenUtc,
s.Detail,
r != null ? r.ConsecutiveFailures : 0,
r != null ? r.LastCircuitBreakerOpenUtc : null,
r != null ? r.LastRecycleUtc : null)).ToListAsync(ct);
return new FocasDriverDetail(instance, config, parseError, hostStatuses);
}
}
/// Projected view of a FOCAS driver's parsed config. Unknown fields are ignored.
public sealed record FocasDriverConfigView
{
public List? Devices { get; set; }
public List? Tags { get; set; }
public FocasProbeView? Probe { get; set; }
public FocasAlarmProjectionView? AlarmProjection { get; set; }
public FocasHandleRecycleView? HandleRecycle { get; set; }
}
public sealed record FocasDeviceView
{
public string? HostAddress { get; set; }
public string? DeviceName { get; set; }
public string? Series { get; set; }
}
public sealed record FocasTagView
{
public string? Name { get; set; }
public string? DeviceHostAddress { get; set; }
public string? Address { get; set; }
public string? DataType { get; set; }
public bool Writable { get; set; } = true;
}
public sealed record FocasProbeView
{
public bool Enabled { get; set; } = true;
public string? Interval { get; set; }
}
public sealed record FocasAlarmProjectionView
{
public bool Enabled { get; set; }
public string? PollInterval { get; set; }
}
public sealed record FocasHandleRecycleView
{
public bool Enabled { get; set; }
public string? Interval { get; set; }
}
/// Composite payload returned to the Admin page.
public sealed record FocasDriverDetail(
DriverInstance Instance,
FocasDriverConfigView? Config,
string? ParseError,
IReadOnlyList HostStatuses);
public sealed record FocasHostStatusRow(
string NodeId,
string HostName,
string State,
DateTime StateChangedUtc,
DateTime LastSeenUtc,
string? Detail,
int ConsecutiveFailures,
DateTime? LastCircuitBreakerOpenUtc,
DateTime? LastRecycleUtc);