diff --git a/gateway/IntegrationGateway.sln b/gateway/IntegrationGateway.sln
index 59a7e01..0775afd 100644
--- a/gateway/IntegrationGateway.sln
+++ b/gateway/IntegrationGateway.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Adapters.Owl", "src\IntegrationGateway.Adapters.Owl\IntegrationGateway.Adapters.Owl.csproj", "{8B0BD376-E1C5-4B62-AEC9-E2F4397C991E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Adapters.MC4", "src\IntegrationGateway.Adapters.MC4\IntegrationGateway.Adapters.MC4.csproj", "{6C64C3EB-58ED-45A3-B86D-0B464081BFA0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +59,18 @@ Global
{8B0BD376-E1C5-4B62-AEC9-E2F4397C991E}.Release|x64.Build.0 = Release|Any CPU
{8B0BD376-E1C5-4B62-AEC9-E2F4397C991E}.Release|x86.ActiveCfg = Release|Any CPU
{8B0BD376-E1C5-4B62-AEC9-E2F4397C991E}.Release|x86.Build.0 = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|x64.Build.0 = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Debug|x86.Build.0 = Debug|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|x64.ActiveCfg = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|x64.Build.0 = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|x86.ActiveCfg = Release|Any CPU
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -65,5 +79,6 @@ Global
{387BD4FC-725B-4948-B413-F50BC6BD605D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{2055B7A5-418F-456C-8642-A99A68282561} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{8B0BD376-E1C5-4B62-AEC9-E2F4397C991E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {6C64C3EB-58ED-45A3-B86D-0B464081BFA0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/IntegrationGateway.Adapters.MC4.csproj b/gateway/src/IntegrationGateway.Adapters.MC4/IntegrationGateway.Adapters.MC4.csproj
new file mode 100644
index 0000000..e0c9f9c
--- /dev/null
+++ b/gateway/src/IntegrationGateway.Adapters.MC4/IntegrationGateway.Adapters.MC4.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs
new file mode 100644
index 0000000..cb5eeed
--- /dev/null
+++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs
@@ -0,0 +1,249 @@
+using IntegrationGateway.Core.Abstractions;
+using IntegrationGateway.Core.Infrastructure;
+using IntegrationGateway.Core.Models;
+using System.Text;
+using System.Text.Json;
+
+namespace IntegrationGateway.Adapters.MC4;
+
+public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
+{
+ private readonly HttpClient _http;
+ private readonly Mc4AuthHelper _auth;
+ private readonly RateLimiter _limiter = new(2);
+
+ public string AdapterCode { get; }
+ public string DisplayName => $"MC4 ({AdapterCode})";
+ public AdapterCapabilities Capabilities => new()
+ {
+ HasObjectTree = true, HasPoints = true, HasAlarms = true, AcceptsControl = true
+ };
+
+ public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl)
+ {
+ AdapterCode = adapterCode;
+ _http = http;
+ _auth = new Mc4AuthHelper(http, baseUrl);
+ }
+
+ public async Task InitializeAsync() => await _auth.GetTokenAsync();
+
+ public async Task HealthCheckAsync()
+ {
+ try
+ {
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var resp = await client.PostAsync("/api/central/auth/conf/get", null);
+ return resp.IsSuccessStatusCode;
+ }
+ catch { return false; }
+ }
+
+ // ─── IHasOwnDeviceTree ───
+ public async Task> GetObjectTreeAsync()
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var resp = await client.PostAsync("/api/central/object/tree", null);
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ var tree = JsonSerializer.Deserialize>(json)!;
+ return tree.Select(MapNode).ToList();
+ }
+
+ private static DeviceTreeNode MapNode(Mc4TreeNode n) => new()
+ {
+ SourceId = n.Id,
+ Name = n.Name ?? n.Id.ToString(),
+ NodeType = n.Type,
+ ObjectType = n.ObjectType,
+ Tag = n.Tag,
+ Option = n.Option ?? new Dictionary(),
+ Children = n.Children?.Select(MapNode).ToList() ?? new()
+ };
+
+ // ─── IHasPoints ───
+ public async Task> GetRealtimeValuesAsync(string sourceDeviceId)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new { id = int.Parse(sourceDeviceId) });
+ var resp = await client.PostAsync("/api/central/device/point/value/get",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ var values = JsonSerializer.Deserialize>(json)!;
+ return values.Select(v => new PointValue
+ {
+ SourceDeviceId = sourceDeviceId,
+ PointIndex = v.Index,
+ Value = v.Value,
+ UpdateTime = v.Time,
+ Interval = v.Interval
+ }).ToList();
+ }
+
+ public async Task> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new
+ {
+ points = points.Select(p => new { id = int.Parse(p.DeviceId), index = p.PointIndex })
+ });
+ var resp = await client.PostAsync("/api/central/point/multi/value/get",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ var values = JsonSerializer.Deserialize>(json)!;
+ return values.Select(v => new PointValue
+ {
+ SourceDeviceId = v.Id.ToString(),
+ PointIndex = v.Index,
+ Value = v.Value,
+ UpdateTime = v.Time,
+ Interval = v.Interval
+ }).ToList();
+ }
+
+ public async Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new { id = int.Parse(sourceDeviceId), index = pointIndex, value });
+ await client.PostAsync("/api/central/point/value/set",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ }
+
+ // ─── IHasAlarms ───
+ public async Task> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
+ int? confirmState = null, int? endState = null, List? levels = null)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new Mc4AlarmQuery
+ {
+ From = from.ToString("yyyy-MM-dd HH:mm:ss"),
+ To = to.ToString("yyyy-MM-dd HH:mm:ss"),
+ Skip = (page - 1) * size,
+ Limit = size,
+ Sort = 1
+ });
+ var resp = await client.PostAsync("/api/central/alarm/query",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ var result = JsonSerializer.Deserialize(json)!;
+
+ return new PagedResult
+ {
+ Items = result.List?.Select(a => new StandardAlarm
+ {
+ AlarmId = a.Id ?? "",
+ DeviceId = a.Sid?.ToString(),
+ AdapterCode = AdapterCode,
+ Level = MapAlarmLevel(a.Level),
+ Title = a.Desc ?? "",
+ Content = a.EngDesc,
+ OccurTime = DateTime.TryParse(a.Stime, out var st) ? st : DateTime.MinValue,
+ Status = MapAlarmState(a.State),
+ ActualValue = a.Soption?.Value,
+ ThresholdValue = a.Eoption?.Value
+ }).ToList() ?? new(),
+ Total = result.Total
+ };
+ }
+
+ public async Task ConfirmAlarmAsync(string alarmId)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
+ await client.PostAsync("/api/central/alarm/confirm",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ }
+
+ public async Task EndAlarmAsync(string alarmId)
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
+ await client.PostAsync("/api/central/alarm/end",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ }
+
+ public async Task GetPendingAlarmCountAsync()
+ {
+ await _limiter.WaitAsync();
+ var client = await _auth.GetAuthenticatedClientAsync();
+ var body = JsonSerializer.Serialize(new { state = 1 });
+ var resp = await client.PostAsync("/api/central/alarm/custom_query_count",
+ new StringContent(body, Encoding.UTF8, "application/json"));
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ return int.TryParse(json, out var count) ? count : 0;
+ }
+
+ private static string MapAlarmLevel(int level) => level switch { 1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示" };
+ private static string MapAlarmState(int state) => state switch { 1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认" };
+}
+
+// ─── MC4 JSON Models ───
+public class Mc4TreeNode
+{
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ public int Type { get; set; }
+ public int ObjectType { get; set; }
+ public string? Tag { get; set; }
+ public Dictionary? Option { get; set; }
+ public List? Children { get; set; }
+}
+
+public class Mc4PointValue
+{
+ public int Id { get; set; }
+ public int Index { get; set; }
+ public double Value { get; set; }
+ public string? Time { get; set; }
+ public int Interval { get; set; }
+}
+
+public class Mc4AlarmQuery
+{
+ public string From { get; set; } = "";
+ public string To { get; set; } = "";
+ public int Skip { get; set; }
+ public int Limit { get; set; }
+ public int Sort { get; set; }
+}
+
+public class Mc4AlarmQueryResult
+{
+ public int Total { get; set; }
+ public List? List { get; set; }
+}
+
+public class Mc4AlarmItem
+{
+ public string? Id { get; set; }
+ public int? Sid { get; set; }
+ public int Sno { get; set; }
+ public string? Desc { get; set; }
+ public string? EngDesc { get; set; }
+ public int Level { get; set; }
+ public int State { get; set; }
+ public string? Stime { get; set; }
+ public string? Etime { get; set; }
+ public string? Ctime { get; set; }
+ public string? Cuser { get; set; }
+ public int Type { get; set; }
+ public Mc4Option? Soption { get; set; }
+ public Mc4Option? Eoption { get; set; }
+}
+
+public class Mc4Option
+{
+ public double? Value { get; set; }
+ public string? TypeName { get; set; }
+}
diff --git a/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs
new file mode 100644
index 0000000..257d76a
--- /dev/null
+++ b/gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs
@@ -0,0 +1,43 @@
+using System.Text.Json;
+
+namespace IntegrationGateway.Adapters.MC4;
+
+public class Mc4AuthHelper
+{
+ private readonly HttpClient _http;
+ private readonly string _baseUrl;
+ private string? _token;
+ private DateTime _tokenExpiry = DateTime.MinValue;
+
+ public Mc4AuthHelper(HttpClient http, string baseUrl)
+ {
+ _http = http;
+ _baseUrl = baseUrl.TrimEnd('/');
+ }
+
+ public async Task GetTokenAsync()
+ {
+ if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
+ return _token;
+
+ var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null);
+ resp.EnsureSuccessStatusCode();
+ var json = await resp.Content.ReadAsStringAsync();
+ var result = JsonSerializer.Deserialize(json);
+ _token = result?.Token ?? "";
+ _tokenExpiry = DateTime.UtcNow.AddHours(8);
+ return _token!;
+ }
+
+ public async Task GetAuthenticatedClientAsync()
+ {
+ var token = await GetTokenAsync();
+ var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
+ client.DefaultRequestHeaders.Add("token", token);
+ return client;
+ }
+
+ public void Invalidate() => _token = null;
+
+ public class Mc4AuthResponse { public string? Token { get; set; } }
+}
diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj
index 283b1a9..526dfbd 100644
--- a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj
+++ b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj
@@ -14,6 +14,7 @@
+
diff --git a/gateway/src/IntegrationGateway.Host/Program.cs b/gateway/src/IntegrationGateway.Host/Program.cs
index 48182a8..7f10585 100644
--- a/gateway/src/IntegrationGateway.Host/Program.cs
+++ b/gateway/src/IntegrationGateway.Host/Program.cs
@@ -1,5 +1,6 @@
using IntegrationGateway.Core.Infrastructure;
using IntegrationGateway.Adapters.Owl;
+using IntegrationGateway.Adapters.MC4;
using IntegrationGateway.Host;
var builder = WebApplication.CreateBuilder(args);
@@ -37,6 +38,17 @@ var owlAdapter = new OwlAdapter(
);
registry.Register(owlAdapter);
+// 注册 MC4 适配器
+var mc4Cfg = builder.Configuration.GetSection("MC4");
+var mc4Http = app.Services.GetRequiredService().CreateClient("VolPro");
+var mc4Adapter = new Mc4Adapter(
+ "MC4:31ku",
+ mc4Http,
+ mc4Cfg["BaseUrl"] ?? "http://localhost:3000"
+);
+registry.Register(mc4Adapter);
+
+
try
{
var result = await gw.RegisterAsync();
diff --git a/gateway/src/IntegrationGateway.Host/appsettings.json b/gateway/src/IntegrationGateway.Host/appsettings.json
index ca1e83a..a1b3097 100644
--- a/gateway/src/IntegrationGateway.Host/appsettings.json
+++ b/gateway/src/IntegrationGateway.Host/appsettings.json
@@ -14,5 +14,8 @@
"BaseUrl": "http://owl_host:15123",
"Username": "admin",
"Password": "your_owl_password"
+ },
+ "MC4": {
+ "BaseUrl": "http://mc4_host:3000"
}
}
\ No newline at end of file