diff --git a/gateway/IntegrationGateway.sln b/gateway/IntegrationGateway.sln new file mode 100644 index 0000000..e575fba --- /dev/null +++ b/gateway/IntegrationGateway.sln @@ -0,0 +1,54 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Host", "src\IntegrationGateway.Host\IntegrationGateway.Host.csproj", "{387BD4FC-725B-4948-B413-F50BC6BD605D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "src\IntegrationGateway.Core\IntegrationGateway.Core.csproj", "{2055B7A5-418F-456C-8642-A99A68282561}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x64.ActiveCfg = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x64.Build.0 = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x86.ActiveCfg = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Debug|x86.Build.0 = Debug|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|Any CPU.Build.0 = Release|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x64.ActiveCfg = Release|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x64.Build.0 = Release|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x86.ActiveCfg = Release|Any CPU + {387BD4FC-725B-4948-B413-F50BC6BD605D}.Release|x86.Build.0 = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|x64.ActiveCfg = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|x64.Build.0 = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|x86.ActiveCfg = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Debug|x86.Build.0 = Debug|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|Any CPU.Build.0 = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|x64.ActiveCfg = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|x64.Build.0 = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.ActiveCfg = Release|Any CPU + {2055B7A5-418F-456C-8642-A99A68282561}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {387BD4FC-725B-4948-B413-F50BC6BD605D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {2055B7A5-418F-456C-8642-A99A68282561} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs new file mode 100644 index 0000000..4e7e41b --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs @@ -0,0 +1,21 @@ +namespace IntegrationGateway.Core.Abstractions; + +public interface IAcceptsMetadataPush : IIntegrationAdapter +{ + Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); +} + +public class MetadataChangeSet +{ + public string? Name { get; set; } + public string? IpAddress { get; set; } + public int? Port { get; set; } + public int? StreamMode { get; set; } +} + +public class MetadataPushResult +{ + public bool Success { get; set; } + public List RejectedFields { get; set; } = new(); + public string? Reason { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs new file mode 100644 index 0000000..038c5f0 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs @@ -0,0 +1,12 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IHasAlarms : IIntegrationAdapter +{ + Task> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, + int? confirmState = null, int? endState = null, List? levels = null); + Task ConfirmAlarmAsync(string alarmId); + Task EndAlarmAsync(string alarmId); + Task GetPendingAlarmCountAsync(); +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs new file mode 100644 index 0000000..830106a --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs @@ -0,0 +1,12 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IHasFlatDevices : IIntegrationAdapter +{ + Task> GetDevicesAsync(int page, int size, string? keyword = null); + Task GetDeviceAsync(string sourceDeviceId); + Task> GetAllDevicesAsync(); + Task> GetChannelsAsync(int page, int size, string? parentDeviceId = null); + Task> GetAllChannelsAsync(); +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs new file mode 100644 index 0000000..c565f01 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs @@ -0,0 +1,8 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IHasOwnDeviceTree : IIntegrationAdapter +{ + Task> GetObjectTreeAsync(); +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs new file mode 100644 index 0000000..796a720 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs @@ -0,0 +1,10 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IHasPoints : IIntegrationAdapter +{ + Task> GetRealtimeValuesAsync(string sourceDeviceId); + Task> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points); + Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs new file mode 100644 index 0000000..ad59126 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs @@ -0,0 +1,14 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IHasStreams : IIntegrationAdapter +{ + Task GetLiveUrlAsync(string channelId); + Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); + Task StopPlayAsync(string channelId); + Task GetSnapshotAsync(string channelId); + Task PtzControlAsync(string channelId, string direction, float speed); + Task PtzStopAsync(string channelId); + Task> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size); +} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs new file mode 100644 index 0000000..51f5ad1 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs @@ -0,0 +1,12 @@ +using IntegrationGateway.Core.Models; + +namespace IntegrationGateway.Core.Abstractions; + +public interface IIntegrationAdapter +{ + string AdapterCode { get; } + string DisplayName { get; } + AdapterCapabilities Capabilities { get; } + Task HealthCheckAsync(); + Task InitializeAsync(); +} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs new file mode 100644 index 0000000..e6edeb2 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs @@ -0,0 +1,27 @@ +using IntegrationGateway.Core.Abstractions; + +namespace IntegrationGateway.Core.Infrastructure; + +public class AdapterRegistry +{ + private readonly Dictionary _adapters = new(); + + public void Register(IIntegrationAdapter adapter) + { + _adapters[adapter.AdapterCode] = adapter; + } + + public IIntegrationAdapter? Get(string adapterCode) + { + _adapters.TryGetValue(adapterCode, out var adapter); + return adapter; + } + + public IEnumerable GetAll() => _adapters.Values; + + public async Task InitializeAllAsync() + { + foreach (var adapter in _adapters.Values) + await adapter.InitializeAsync(); + } +} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs new file mode 100644 index 0000000..ac595af --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs @@ -0,0 +1,27 @@ +namespace IntegrationGateway.Core.Infrastructure; + +public class RateLimiter +{ + private readonly SemaphoreSlim _semaphore; + private readonly int _minIntervalMs; + private DateTime _lastRequest = DateTime.MinValue; + + public RateLimiter(int maxCallsPerSecond) + { + _semaphore = new SemaphoreSlim(maxCallsPerSecond, maxCallsPerSecond); + _minIntervalMs = 1000 / maxCallsPerSecond; + } + + public async Task WaitAsync() + { + await _semaphore.WaitAsync(); + try + { + var elapsed = (int)(DateTime.UtcNow - _lastRequest).TotalMilliseconds; + if (elapsed < _minIntervalMs) + await Task.Delay(_minIntervalMs - elapsed); + _lastRequest = DateTime.UtcNow; + } + finally { _semaphore.Release(); } + } +} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs new file mode 100644 index 0000000..283accf --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace IntegrationGateway.Core.Infrastructure; + +public class TokenManager +{ + private readonly IMemoryCache _cache; + private static readonly SemaphoreSlim _semaphore = new(1, 1); + + public TokenManager(IMemoryCache cache) => _cache = cache; + + public async Task GetAsync(string key) + { + _cache.TryGetValue($"token_{key}", out string? token); + return token; + } + + public async Task SetAsync(string key, string token, TimeSpan expiresIn) + { + await _semaphore.WaitAsync(); + try { _cache.Set($"token_{key}", token, expiresIn * 0.9); } + finally { _semaphore.Release(); } + } + + public void Remove(string key) => _cache.Remove($"token_{key}"); +} diff --git a/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj b/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj new file mode 100644 index 0000000..985d3af --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs b/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs new file mode 100644 index 0000000..67b86df --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs @@ -0,0 +1,14 @@ +namespace IntegrationGateway.Core.Models; + +public class AdapterCapabilities +{ + public bool HasObjectTree { get; set; } + public bool HasFlatDevices { get; set; } + public bool HasPoints { get; set; } + public bool HasStreams { get; set; } + public bool HasAlarms { get; set; } + public bool HasRecordings { get; set; } + public bool HasPtz { get; set; } + public bool AcceptsControl { get; set; } + public bool AcceptsMetadataPush { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs b/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs new file mode 100644 index 0000000..fe8e09f --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs @@ -0,0 +1,13 @@ +namespace IntegrationGateway.Core.Models; + +public class DeviceTreeNode +{ + public int SourceId { get; set; } + public string Name { get; set; } = ""; + public int NodeType { get; set; } + public int ObjectType { get; set; } + public string? Tag { get; set; } + public Dictionary Option { get; set; } = new(); + public List Children { get; set; } = new(); + public string? ParentPath { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs b/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs new file mode 100644 index 0000000..90e077a --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs @@ -0,0 +1,7 @@ +namespace IntegrationGateway.Core.Models; + +public class PagedResult +{ + public List Items { get; set; } = new(); + public int Total { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/PointValue.cs b/gateway/src/IntegrationGateway.Core/Models/PointValue.cs new file mode 100644 index 0000000..133f376 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/PointValue.cs @@ -0,0 +1,11 @@ +namespace IntegrationGateway.Core.Models; + +public class PointValue +{ + public string SourceDeviceId { get; set; } = ""; + public int PointIndex { get; set; } + public double Value { get; set; } + public string? UpdateTime { get; set; } + public int Interval { get; set; } + public bool IsValid { get; set; } = true; +} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs b/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs new file mode 100644 index 0000000..8b1c857 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs @@ -0,0 +1,15 @@ +namespace IntegrationGateway.Core.Models; + +public class StandardAlarm +{ + public string AlarmId { get; set; } = ""; + public string? DeviceId { get; set; } + public string AdapterCode { get; set; } = ""; + public string Level { get; set; } = ""; + public string Title { get; set; } = ""; + public string? Content { get; set; } + public DateTime OccurTime { get; set; } + public string Status { get; set; } = "Active"; + public double? ThresholdValue { get; set; } + public double? ActualValue { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs b/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs new file mode 100644 index 0000000..93e9b70 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs @@ -0,0 +1,17 @@ +namespace IntegrationGateway.Core.Models; + +public class StandardDevice +{ + public string SourceId { get; set; } = ""; + public string AdapterCode { get; set; } = ""; + public string Name { get; set; } = ""; + public string Category { get; set; } = ""; + public string Group { get; set; } = ""; + public string? IpAddress { get; set; } + public int? Port { get; set; } + public bool IsOnline { get; set; } + public bool IsParent { get; set; } + public string? ParentSourceId { get; set; } + public Dictionary Extra { get; set; } = new(); + public DateTime LastSyncTime { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs b/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs new file mode 100644 index 0000000..cb1b578 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs @@ -0,0 +1,13 @@ +namespace IntegrationGateway.Core.Models; + +public class StandardPoint +{ + public string SourceDeviceId { get; set; } = ""; + public int PointIndex { get; set; } + public int PointType { get; set; } + public string? PointTag { get; set; } + public string PointName { get; set; } = ""; + public string? PointDesc { get; set; } + public string? Unit { get; set; } + public bool IsControlPoint { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs b/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs new file mode 100644 index 0000000..8c3ade1 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs @@ -0,0 +1,12 @@ +namespace IntegrationGateway.Core.Models; + +public class StandardRecording +{ + public string Id { get; set; } = ""; + public string ChannelId { get; set; } = ""; + public DateTime StartedAt { get; set; } + public DateTime EndedAt { get; set; } + public double Duration { get; set; } + public string? FilePath { get; set; } + public long Size { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs b/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs new file mode 100644 index 0000000..339583a --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs @@ -0,0 +1,11 @@ +namespace IntegrationGateway.Core.Models; + +public class StreamUrls +{ + public string? WsFlv { get; set; } + public string? HttpFlv { get; set; } + public string? Hls { get; set; } + public string? WebRtc { get; set; } + public string? Rtmp { get; set; } + public string? Rtsp { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs b/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs new file mode 100644 index 0000000..572bd75 --- /dev/null +++ b/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs @@ -0,0 +1,13 @@ +namespace IntegrationGateway.Core.Models; + +public class SyncReport +{ + public string AdapterCode { get; set; } = ""; + public int Added { get; set; } + public int Updated { get; set; } + public int Skipped { get; set; } + public int Removed { get; set; } + public List Errors { get; set; } = new(); + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs new file mode 100644 index 0000000..f9b19fe --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs @@ -0,0 +1,32 @@ +using IntegrationGateway.Core.Abstractions; +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway/alarms")] +public class AlarmsController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public AlarmsController(AdapterRegistry registry) => _registry = registry; + + [HttpGet("{adapter}")] + public async Task GetAlarms(string adapter, + [FromQuery] DateTime from, [FromQuery] DateTime to, + [FromQuery] int page = 1, [FromQuery] int size = 50) + { + var a = _registry.Get(adapter); + if (a is not IHasAlarms al) return NotFound(); + return Ok(await al.GetAlarmsAsync(page, size, from, to)); + } + + [HttpPost("{adapter}/{alarmId}/confirm")] + public async Task Confirm(string adapter, string alarmId) + { + var a = _registry.Get(adapter); + if (a is not IHasAlarms al) return NotFound(); + await al.ConfirmAlarmAsync(alarmId); + return Ok(new { status = "confirmed" }); + } +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs b/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs new file mode 100644 index 0000000..bc32491 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs @@ -0,0 +1,30 @@ +using IntegrationGateway.Core.Abstractions; +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway/devices")] +public class DevicesController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public DevicesController(AdapterRegistry registry) => _registry = registry; + + [HttpGet] + public async Task GetDevices([FromQuery] string adapter, [FromQuery] int page = 1, [FromQuery] int size = 50) + { + var a = _registry.Get(adapter); + if (a is not IHasFlatDevices f) return NotFound(); + return Ok(await f.GetDevicesAsync(page, size)); + } + + [HttpGet("{adapter}/{deviceId}")] + public async Task GetDevice(string adapter, string deviceId) + { + var a = _registry.Get(adapter); + if (a is not IHasFlatDevices f) return NotFound(); + var d = await f.GetDeviceAsync(deviceId); + return d is null ? NotFound() : Ok(d); + } +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs b/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs new file mode 100644 index 0000000..bdb9a6d --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs @@ -0,0 +1,21 @@ +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway/health")] +public class HealthController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public HealthController(AdapterRegistry registry) => _registry = registry; + + [HttpGet] + public async Task Get() + { + var status = new Dictionary(); + foreach (var a in _registry.GetAll()) + status[a.AdapterCode] = await a.HealthCheckAsync(); + return Ok(new { gateway = "ok", adapters = status }); + } +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs new file mode 100644 index 0000000..1298dc8 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs @@ -0,0 +1,37 @@ +using IntegrationGateway.Core.Abstractions; +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway/realtime")] +public class PointsController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public PointsController(AdapterRegistry registry) => _registry = registry; + + [HttpGet("{adapter}/{deviceId}")] + public async Task GetRealtime(string adapter, string deviceId) + { + var a = _registry.Get(adapter); + if (a is not IHasPoints p) return NotFound(); + return Ok(await p.GetRealtimeValuesAsync(deviceId)); + } + + [HttpPost("{adapter}/control")] + public async Task Control(string adapter, [FromBody] ControlRequest req) + { + var a = _registry.Get(adapter); + if (a is not IHasPoints p) return NotFound(); + await p.SetPointValueAsync(req.DeviceSourceId, req.PointIndex, req.Value); + return Ok(new { status = "sent" }); + } +} + +public class ControlRequest +{ + public string DeviceSourceId { get; set; } = ""; + public int PointIndex { get; set; } + public double Value { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/RegisterController.cs b/gateway/src/IntegrationGateway.Host/Controllers/RegisterController.cs new file mode 100644 index 0000000..90e4008 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/RegisterController.cs @@ -0,0 +1,40 @@ +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway")] +public class RegisterController : ControllerBase +{ + private readonly AdapterRegistry _registry; + + public RegisterController(AdapterRegistry registry) => _registry = registry; + + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest req) + { + // 网关向 Vol.Pro 注册,由外部 GatewayClient 调用 + return Ok(new { status = "ok" }); + } + + [HttpPost("heartbeat")] + public async Task Heartbeat([FromBody] HeartbeatRequest req) + { + return Ok(new { status = "ok" }); + } +} + +public class RegisterRequest +{ + public string NodeCode { get; set; } = ""; + public string Token { get; set; } = ""; + public string AdapterTypes { get; set; } = ""; + public string BaseUrl { get; set; } = ""; +} + +public class HeartbeatRequest +{ + public string NodeCode { get; set; } = ""; + public string Token { get; set; } = ""; +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs new file mode 100644 index 0000000..5ad9d1e --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs @@ -0,0 +1,48 @@ +using IntegrationGateway.Core.Abstractions; +using IntegrationGateway.Core.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway/streams")] +public class StreamsController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public StreamsController(AdapterRegistry registry) => _registry = registry; + + [HttpGet("{adapter}/{channelId}/live")] + public async Task GetLive(string adapter, string channelId) + { + var a = _registry.Get(adapter); + if (a is not IHasStreams s) return NotFound(); + return Ok(await s.GetLiveUrlAsync(channelId)); + } + + [HttpGet("{adapter}/{channelId}/playback")] + public async Task GetPlayback(string adapter, string channelId, + [FromQuery] DateTime start, [FromQuery] DateTime end) + { + var a = _registry.Get(adapter); + if (a is not IHasStreams s) return NotFound(); + return Ok(await s.GetPlaybackUrlAsync(channelId, start, end)); + } + + [HttpPost("{adapter}/{channelId}/ptz")] + public async Task Ptz(string adapter, string channelId, [FromBody] PtzRequest req) + { + var a = _registry.Get(adapter); + if (a is not IHasStreams s) return NotFound(); + if (req.Direction == "stop") + await s.PtzStopAsync(channelId); + else + await s.PtzControlAsync(channelId, req.Direction, req.Speed); + return Ok(new { status = "ok" }); + } +} + +public class PtzRequest +{ + public string Direction { get; set; } = "stop"; + public float Speed { get; set; } = 0.5f; +} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs b/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs new file mode 100644 index 0000000..e2d5a98 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs @@ -0,0 +1,49 @@ +using IntegrationGateway.Core.Abstractions; +using IntegrationGateway.Core.Infrastructure; +using IntegrationGateway.Core.Models; +using Microsoft.AspNetCore.Mvc; + +namespace IntegrationGateway.Host.Controllers; + +[ApiController] +[Route("api/gateway")] +public class SyncController : ControllerBase +{ + private readonly AdapterRegistry _registry; + public SyncController(AdapterRegistry registry) => _registry = registry; + + [HttpPost("devices/sync")] + public async Task SyncDevices([FromQuery] string adapter) + { + var a = _registry.Get(adapter) + ?? throw new InvalidOperationException($"Adapter '{adapter}' not found"); + + var report = new SyncReport { AdapterCode = adapter, StartTime = DateTime.UtcNow }; + + if (a is IHasFlatDevices f) + { + var devices = await f.GetAllDevicesAsync(); + report.Added = devices.Count; + } + else if (a is IHasOwnDeviceTree t) + { + var tree = await t.GetObjectTreeAsync(); + report.Added = CountDeviceNodes(tree); + } + else return BadRequest("Adapter does not support device sync"); + + report.EndTime = DateTime.UtcNow; + return Ok(report); + } + + private int CountDeviceNodes(List nodes) + { + int count = 0; + foreach (var n in nodes) + { + if (n.NodeType == 2) count++; + count += CountDeviceNodes(n.Children); + } + return count; + } +} diff --git a/gateway/src/IntegrationGateway.Host/GatewayClient.cs b/gateway/src/IntegrationGateway.Host/GatewayClient.cs new file mode 100644 index 0000000..e37bfc5 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/GatewayClient.cs @@ -0,0 +1,118 @@ +using System.Text; +using System.Text.Json; + +namespace IntegrationGateway.Host; + +public class GatewayClient +{ + private readonly HttpClient _http; + private readonly IConfiguration _config; + + public GatewayClient(IHttpClientFactory factory, IConfiguration config) + { + _http = factory.CreateClient("VolPro"); + _config = config; + } + + /// A1: 网关注册 + public async Task RegisterAsync() + { + var resp = await _http.PostAsJsonAsync("/api/gateway/register", new + { + nodeCode = _config["NodeCode"], + token = _config["NodeToken"], + adapterTypes = GetAdapterTypes(), + baseUrl = _config["Urls"]?.Replace("http://*:", $"http://localhost:") + }); + resp.EnsureSuccessStatusCode(); + return await resp.Content.ReadFromJsonAsync(); + } + + /// A2: 心跳 + public async Task HeartbeatAsync() + { + await _http.PostAsJsonAsync("/api/gateway/heartbeat", new + { + nodeCode = _config["NodeCode"], + token = _config["NodeToken"] + }); + } + + /// A3: 设备同步 + public async Task SyncDevicesAsync(List devices) + { + var resp = await _http.PostAsJsonAsync("/api/gateway/sync/devices", new + { + nodeCode = _config["NodeCode"], + token = _config["NodeToken"], + devices + }); + resp.EnsureSuccessStatusCode(); + return await resp.Content.ReadFromJsonAsync() ?? new(); + } + + /// A4: 告警同步 + public async Task SyncAlarmsAsync(List alarms) + { + var resp = await _http.PostAsJsonAsync("/api/gateway/sync/alarms", new + { + nodeCode = _config["NodeCode"], + token = _config["NodeToken"], + alarms + }); + resp.EnsureSuccessStatusCode(); + return await resp.Content.ReadFromJsonAsync() ?? new(); + } + + private string GetAdapterTypes() => "MC4,Owl"; +} + +public class RegisterResponse +{ + public int NodeId { get; set; } + public List Devices { get; set; } = new(); +} + +public class DeviceItem +{ + public int DeviceId { get; set; } + public string DeviceName { get; set; } = ""; + public string AdapterCode { get; set; } = ""; + public string SourceId { get; set; } = ""; + public string DeviceCategory { get; set; } = ""; + public string DeviceGroup { get; set; } = ""; + public string IsParent { get; set; } = ""; + public string IsOnline { get; set; } = ""; + public JsonElement? ExtraData { get; set; } +} + +public class DeviceSyncItem +{ + public string AdapterCode { get; set; } = ""; + public string SourceId { get; set; } = ""; + public string Name { get; set; } = ""; + public string Category { get; set; } = ""; + public string Group { get; set; } = ""; + public bool IsParent { get; set; } + public string? ParentSourceId { get; set; } + public bool IsOnline { get; set; } + public string? IpAddress { get; set; } + public int? Port { get; set; } + public Dictionary? ExtraData { get; set; } +} + +public class AlarmSyncItem +{ + public string SourceAlarmId { get; set; } = ""; + public string DeviceSourceId { get; set; } = ""; + public string AdapterCode { get; set; } = ""; + public string Level { get; set; } = ""; + public string Desc { get; set; } = ""; + public double? Value { get; set; } + public string StartTime { get; set; } = ""; +} + +public class SyncResult +{ + public int Added { get; set; } +} diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj new file mode 100644 index 0000000..441a01e --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http new file mode 100644 index 0000000..fb61b5a --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http @@ -0,0 +1,6 @@ +@IntegrationGateway.Host_HostAddress = http://localhost:5169 + +GET {{IntegrationGateway.Host_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/gateway/src/IntegrationGateway.Host/Program.cs b/gateway/src/IntegrationGateway.Host/Program.cs new file mode 100644 index 0000000..35560f0 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Program.cs @@ -0,0 +1,38 @@ +using IntegrationGateway.Core.Infrastructure; +using IntegrationGateway.Host; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddMemoryCache(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHttpClient("VolPro", c => +{ + c.BaseAddress = new Uri(builder.Configuration["VolProBaseUrl"] ?? "http://localhost:9100"); +}); +builder.Services.AddSingleton(); + +var app = builder.Build(); +app.MapControllers(); + +// 启动时自动向 Vol.Pro 注册 +app.Lifetime.ApplicationStarted.Register(() => +{ + Task.Run(async () => + { + var gw = app.Services.GetRequiredService(); + var registry = app.Services.GetRequiredService(); + try + { + var result = await gw.RegisterAsync(); + Console.WriteLine($"[Gateway] Registered as NodeId={result?.NodeId}, Devices={result?.Devices.Count ?? 0}"); + } + catch (Exception ex) + { + Console.WriteLine($"[Gateway] Registration failed: {ex.Message}"); + } + }); +}); + +app.Run(); diff --git a/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json b/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json new file mode 100644 index 0000000..77c6716 --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:4117", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5169", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/gateway/src/IntegrationGateway.Host/appsettings.Development.json b/gateway/src/IntegrationGateway.Host/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/gateway/src/IntegrationGateway.Host/appsettings.json b/gateway/src/IntegrationGateway.Host/appsettings.json new file mode 100644 index 0000000..376406d --- /dev/null +++ b/gateway/src/IntegrationGateway.Host/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Urls": "http://*:5100", + "VolProBaseUrl": "http://localhost:9100", + "NodeCode": "gw-31ku", + "NodeToken": "xxxxxxxxxx" +} \ No newline at end of file