diff --git a/src/CasaBot/AutoScan/AutoScanApp.cs b/src/CasaBot/AutoScan/AutoScanApp.cs index b036fcc..3364ce7 100644 --- a/src/CasaBot/AutoScan/AutoScanApp.cs +++ b/src/CasaBot/AutoScan/AutoScanApp.cs @@ -72,7 +72,7 @@ public class AutoScanApp .AddJob(downloaderJob) .AddJob(scannerJob) .AddJob(cleanJob) - .OnFinish(async () => await OnScanCompleted?.Invoke()) + .OnFinish(OnScanCompleted) .Build(); _scheduler.ListenerManager.AddJobListener(chainer, GroupMatcher.GroupEquals(GROUP_NAME)); @@ -107,7 +107,6 @@ public class AutoScanApp return path; } - private string CronFromAt(string at) { var parts = at.Split(':'); diff --git a/src/CasaBot/AutoScan/DependencyInjectionExtensions.cs b/src/CasaBot/AutoScan/DependencyInjectionExtensions.cs index 316a366..4a4e782 100644 --- a/src/CasaBot/AutoScan/DependencyInjectionExtensions.cs +++ b/src/CasaBot/AutoScan/DependencyInjectionExtensions.cs @@ -17,6 +17,7 @@ public static class DependencyInjectionExtensions services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddQuartz(); services.AddTransient(); diff --git a/src/CasaBot/AutoScan/Implementations/ShinobiConnector.cs b/src/CasaBot/AutoScan/Implementations/ShinobiConnector.cs index f8090a6..5b961aa 100644 --- a/src/CasaBot/AutoScan/Implementations/ShinobiConnector.cs +++ b/src/CasaBot/AutoScan/Implementations/ShinobiConnector.cs @@ -95,7 +95,7 @@ public class ShinobiConnector : IDVRConnector _logger.LogDebug("Fetching video stream from endpoint: {Endpoint}", endpoint); var monitors = await _httpClient.GetFromJsonAsync(endpoint); - _cachedVideoStream = monitors?.FirstOrDefault()?.MonitorDetail?.auto_host ?? null; + _cachedVideoStream = monitors?.FirstOrDefault()?.MonitorDetail?.MonitorStreamUrl ?? null; return _cachedVideoStream ?? ""; } } \ No newline at end of file diff --git a/src/CasaBot/AutoScan/Implementations/Snapshoter.cs b/src/CasaBot/AutoScan/Implementations/Snapshoter.cs new file mode 100644 index 0000000..3605957 --- /dev/null +++ b/src/CasaBot/AutoScan/Implementations/Snapshoter.cs @@ -0,0 +1,96 @@ +using AutoScan.Interfaces; +using AutoScan.Options; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace AutoScan.Implementations; + +public class Snapshoter : ISnapshoter +{ + private readonly IDVRConnector _dvrConnector; + private readonly AutoScanOptions _options; + private readonly ILogger _logger; + + public Snapshoter(IDVRConnector dvrConnector, IOptions options, ILogger logger) + { + _dvrConnector = dvrConnector; + _options = options.Value; + _logger = logger; + } + + public async Task TakeSnapshot() + { + try + { + if (_options.Scanner?.FFMpeg is null) + { + _logger.LogError("FFMpeg path is not set in the options"); + return null; + } + + var timer = new Stopwatch(); + timer.Start(); + var outputDir = _options.Scanner?.SnapshotFolder; + if (string.IsNullOrEmpty(outputDir)) + { + _logger.LogError("Snapshot folder is not set in the options"); + return null; + } + var outputPath = Path.Combine(outputDir, "snp.jpeg"); + //create if doesnt exists + if (!Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + var originalFeed = await _dvrConnector.GetVideoStream(); + var ffmArgs = $"-y -rtsp_transport tcp -i \"{originalFeed}\" -ss 00:00:00.500 -frames:v 1 {outputPath}"; + + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + //To change this, I need to make sure ffmpeg is installed + FileName = _options.Scanner?.FFMpeg, + Arguments = ffmArgs, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + + var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(6)); + try + { + await process.WaitForExitAsync(timeoutSignal.Token); + } catch (OperationCanceledException) + { + _logger.LogError("Taking snapshot timed out"); + process.Kill(); + return null; + } + + // you can read the output here. + // var output = await process.StandardOutput.ReadToEndAsync(); + // var error = await process.StandardError.ReadToEndAsync(); + timer.Stop(); + _logger.LogDebug("Taking snapshot took {Elapsed} ms", timer.ElapsedMilliseconds); + + if (process.ExitCode != 0) + { + _logger.LogError("Error taking snapshot, exit code: {ExitCode}", process.ExitCode); + return string.Empty; + } + + return outputPath; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error taking snapshot"); + return null; + } + } +} \ No newline at end of file diff --git a/src/CasaBot/AutoScan/Interfaces/ISnapshoter.cs b/src/CasaBot/AutoScan/Interfaces/ISnapshoter.cs new file mode 100644 index 0000000..0b3bd36 --- /dev/null +++ b/src/CasaBot/AutoScan/Interfaces/ISnapshoter.cs @@ -0,0 +1,6 @@ +namespace AutoScan.Interfaces; + +public interface ISnapshoter +{ + Task TakeSnapshot(); +} \ No newline at end of file diff --git a/src/CasaBot/AutoScan/Listener/ChainerListener.cs b/src/CasaBot/AutoScan/Listener/ChainerListener.cs index 5fe80b1..0dab405 100644 --- a/src/CasaBot/AutoScan/Listener/ChainerListener.cs +++ b/src/CasaBot/AutoScan/Listener/ChainerListener.cs @@ -9,7 +9,7 @@ public class ChainerListener : JobListenerSupport { private readonly ILogger _logger; private readonly Dictionary _chainLinks; - public Func OnJobChainFinished { get; set; } + public Func? OnJobChainFinished { get; set; } public ChainerListener(string name, ILogger logger) { @@ -56,7 +56,8 @@ public class ChainerListener : JobListenerSupport if (_chainLinks.ContainsValue(context.JobDetail.Key)) { _logger.LogInformation("Job '{JobKey}' is the last in the chain", context.JobDetail.Key); - await OnJobChainFinished?.Invoke(); + if (OnJobChainFinished is not null) + await OnJobChainFinished.Invoke(); return; } } diff --git a/src/CasaBot/AutoScan/Listener/ChainerListenerBuilder.cs b/src/CasaBot/AutoScan/Listener/ChainerListenerBuilder.cs index d367054..c60055c 100644 --- a/src/CasaBot/AutoScan/Listener/ChainerListenerBuilder.cs +++ b/src/CasaBot/AutoScan/Listener/ChainerListenerBuilder.cs @@ -8,7 +8,7 @@ public class ChainerListenerBuilder private readonly List _chain = []; - private Func _finishCallback = () => Task.CompletedTask; + private Func? _finishCallback = () => Task.CompletedTask; public ChainerListenerBuilder(ChainerListener chainerListener) { @@ -21,7 +21,7 @@ public class ChainerListenerBuilder return this; } - public ChainerListenerBuilder OnFinish(Func callback) + public ChainerListenerBuilder OnFinish(Func? callback) { _finishCallback = callback; return this; diff --git a/src/CasaBot/AutoScan/Models/Monitor.cs b/src/CasaBot/AutoScan/Models/Monitor.cs index 2dc30b5..0e405ec 100644 --- a/src/CasaBot/AutoScan/Models/Monitor.cs +++ b/src/CasaBot/AutoScan/Models/Monitor.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; namespace AutoScan.Models; @@ -13,12 +14,16 @@ public class Monitor public int port { get; init; } public string? path { get; init; } - public MonitorDetail? MonitorDetail => JsonSerializer.Deserialize(details); + public MonitorDetail? MonitorDetail => + details != null ? JsonSerializer.Deserialize(details) : null; } public class MonitorDetail { - public string auto_host { get; set; } - public string muser { get; set; } - public string mpass { get; set; } + [JsonPropertyName("auto_host")] + public string? MonitorStreamUrl { get; set; } + [JsonPropertyName("muser")] + public string? Username { get; set; } + [JsonPropertyName("mpass")] + public string? Password { get; set; } } \ No newline at end of file diff --git a/src/CasaBot/AutoScan/Options/ScannerOptions.cs b/src/CasaBot/AutoScan/Options/ScannerOptions.cs index 70dadd2..fd72ebf 100644 --- a/src/CasaBot/AutoScan/Options/ScannerOptions.cs +++ b/src/CasaBot/AutoScan/Options/ScannerOptions.cs @@ -3,7 +3,9 @@ namespace AutoScan.Options; public class ScannerOptions { public string? Exe { get; set; } + public string? FFMpeg { get; set; } public string? ConfigFile { get; set; } public string? DetectionFolder { get; set; } + public string? SnapshotFolder { get; set; } public bool RunDry { get; set; } = false; } \ No newline at end of file diff --git a/src/CasaBot/CasaBotApp/CasaBotApp.csproj b/src/CasaBot/CasaBotApp/CasaBotApp.csproj index 0a73383..307f690 100644 --- a/src/CasaBot/CasaBotApp/CasaBotApp.csproj +++ b/src/CasaBot/CasaBotApp/CasaBotApp.csproj @@ -44,6 +44,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/CasaBot/CasaBotApp/Dockerfile b/src/CasaBot/CasaBotApp/Dockerfile index 037ccd0..d6120f3 100644 --- a/src/CasaBot/CasaBotApp/Dockerfile +++ b/src/CasaBot/CasaBotApp/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /source #COPY --link *.csproj . COPY ["CasaBotApp/CasaBotApp.csproj", "CasaBotApp/"] COPY ["AutoScan/AutoScan.csproj", "AutoScan/"] +COPY ["ControlServer/ControlServer.csproj", "ControlServer/"] RUN dotnet restore "CasaBotApp/CasaBotApp.csproj" -a $TARGETARCH # Copy source code and publish app @@ -31,6 +32,8 @@ RUN apt-get install -y python3 python3-pip #RUN apt-get update && apt-get install -y python3 python3-pip RUN python3 -m pip install dvr-scan[opencv-headless] --break-system-packages +RUN apt-get update && apt-get install -y ffmpeg + COPY --link --from=build /app . COPY CasaBotApp/appsettings.Docker.json ./appsettings.json ENTRYPOINT ["dotnet", "CasaBotApp.dll"] diff --git a/src/CasaBot/CasaBotApp/Extensions/AlarmBotOrquestrator.cs b/src/CasaBot/CasaBotApp/Extensions/AlarmBotOrquestrator.cs index a5284f2..59135bc 100644 --- a/src/CasaBot/CasaBotApp/Extensions/AlarmBotOrquestrator.cs +++ b/src/CasaBot/CasaBotApp/Extensions/AlarmBotOrquestrator.cs @@ -16,16 +16,17 @@ public class AlarmBotOrquestrator private readonly AutoScanApp _autoScanApp; private readonly IControlServer _controlServer; private readonly IShinobiLinkFactory _shinobiLinkFactory; - private readonly IDVRConnector _dvrConnector; + private ISnapshoter _snapshoter; - public AlarmBotOrquestrator(ILogger logger, BotHandler botHandler, AutoScanApp autoScanApp, IControlServer controlServer, IShinobiLinkFactory shinobiLinkFactory, IDVRConnector dvrConnector) + public AlarmBotOrquestrator(ILogger logger, BotHandler botHandler, AutoScanApp autoScanApp, + IControlServer controlServer, IShinobiLinkFactory shinobiLinkFactory, ISnapshoter snapshoter) { _logger = logger; _botHandler = botHandler; _autoScanApp = autoScanApp; _controlServer = controlServer; _shinobiLinkFactory = shinobiLinkFactory; - _dvrConnector = dvrConnector; + _snapshoter = snapshoter; } public void RegisterCommands() @@ -73,7 +74,7 @@ public class AlarmBotOrquestrator { var stopwatch = Stopwatch.StartNew(); stopwatch.Start(); - var outputPath = await TakeSnapshot(); + var outputPath = await _snapshoter.TakeSnapshot(); stopwatch.Stop(); if (string.IsNullOrEmpty(outputPath)) { @@ -139,69 +140,33 @@ public class AlarmBotOrquestrator public void RegisterControlServer() { - _controlServer.OnEvent(async se => + _controlServer.OnEvent(async sensorEvent => { - var mediaPath = await TakeSnapshot(); + var mediaPath = await _snapshoter.TakeSnapshot(); if (string.IsNullOrEmpty(mediaPath)) { await _botHandler.AlertText("Unauthorized access detected 🚨🚨🚨, but no media available"); return; } - if (se.Type == EventType.Fired) + if (sensorEvent.Type == EventType.Fired) { await _botHandler.AlertPhoto(mediaPath, "Unauthorized access detected 🚨 🚨 🚨", [ new(OptionType.Url, "Camera Feed", _shinobiLinkFactory.BuildFeedLink()), - new(OptionType.Action, "Authorize", $"authorize-{se.EventId}", (data, chatId ) => + new(OptionType.Action, "Authorize", $"authorize-{sensorEvent.EventId}", (_, _ ) => { - _logger.LogWarning("Authorizing event {EventId}", se.EventId); - _controlServer.AuthorizeEvent(se.EventId); - return Task.FromResult("Authorization not implemented"); + _logger.LogWarning("Authorizing event {EventId}", sensorEvent.EventId); + _controlServer.AuthorizeEvent(sensorEvent.EventId); + return Task.FromResult("Entrance authorized"); }), ]); } - if (se.Type == EventType.DisarmedEntrance) + if (sensorEvent.Type == EventType.DisarmedEntrance) { await _botHandler.UpdateText("Authorize access"); } }); } - private async Task TakeSnapshot() - { - var timer = new Stopwatch(); - timer.Start(); - var outputPath = Path.Combine(".", "media", "snp", "something.jpeg"); - var originalFeed = await _dvrConnector.GetVideoStream(); - var ffmArgs = $"-y -i \"{originalFeed}\" -ss 00:00:00.500 -vframes 1 {outputPath}"; - - var process = new Process - { - StartInfo = new ProcessStartInfo - { - //To change this, I need to make sure ffmpeg is installed - FileName = "./dvr-scanner/ffmpeg.exe", - Arguments = ffmArgs, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - } - }; - process.Start(); - await process.WaitForExitAsync(); - // You can read the output here. - // var output = await process.StandardOutput.ReadToEndAsync(); - // var error = await process.StandardError.ReadToEndAsync(); - timer.Stop(); - _logger.LogDebug("Taking snapshot took {Elapsed} ms", timer.ElapsedMilliseconds); - - if(process.ExitCode != 0) - { - _logger.LogError("Error taking snapshot, exit code: {ExitCode}", process.ExitCode); - return string.Empty; - } - return outputPath; - } } \ No newline at end of file diff --git a/src/CasaBot/CasaBotApp/appsettings.Docker.json b/src/CasaBot/CasaBotApp/appsettings.Docker.json index 2c0acce..8d3a7ae 100644 --- a/src/CasaBot/CasaBotApp/appsettings.Docker.json +++ b/src/CasaBot/CasaBotApp/appsettings.Docker.json @@ -29,8 +29,10 @@ "MediaFolder": "./media/originals/", "Scanner": { "Exe": "dvr-scan", + "FFMpeg": "ffmpeg", "ConfigFile": "./dvr-scan.cfg", "DetectionFolder": "./media/detections/", + "SnapshotFolder": "./media/snapshots/", "RunDry": false } }, diff --git a/src/CasaBot/CasaBotApp/appsettings.json b/src/CasaBot/CasaBotApp/appsettings.json index 56de868..b90e1e4 100644 --- a/src/CasaBot/CasaBotApp/appsettings.json +++ b/src/CasaBot/CasaBotApp/appsettings.json @@ -30,8 +30,10 @@ "MediaFolder": "./media/originals/", "Scanner": { "Exe": "./dvr-scanner/dvr-scan.exe", + "FFMpeg": "./dvr-scanner/ffmpeg.exe", "ConfigFile": "./dvr-scanner/dvr-scan.cfg", "DetectionFolder": "./media/detections/", + "SnapshotFolder": "./media/snapshots/", "RunDry": false }, "RemoveOriginalFiles": false,