feat: add support for http server control and entrance sensor
This commit is contained in:
parent
620096a7b1
commit
4507f5ab21
@ -13,6 +13,7 @@ public static class DependencyInjectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddSingleton<IDVRConnector, ShinobiConnector>();
|
services.AddSingleton<IDVRConnector, ShinobiConnector>();
|
||||||
services.AddSingleton<IDVRScanner, DVRScanner>();
|
services.AddSingleton<IDVRScanner, DVRScanner>();
|
||||||
|
services.AddSingleton<IShinobiLinkFactory, ShinobiLinkFactory>();
|
||||||
|
|
||||||
services.AddSingleton<FfmpegWrapper>();
|
services.AddSingleton<FfmpegWrapper>();
|
||||||
services.AddSingleton<AutoScanApp>();
|
services.AddSingleton<AutoScanApp>();
|
||||||
|
@ -4,6 +4,7 @@ using AutoScan.Options;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using Monitor = AutoScan.Models.Monitor;
|
||||||
|
|
||||||
namespace AutoScan.Implementations;
|
namespace AutoScan.Implementations;
|
||||||
|
|
||||||
@ -11,19 +12,20 @@ public class ShinobiConnector : IDVRConnector
|
|||||||
{
|
{
|
||||||
private readonly ILogger<ShinobiConnector> _logger;
|
private readonly ILogger<ShinobiConnector> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ShinobiOptions _options;
|
|
||||||
|
|
||||||
public ShinobiConnector(ILogger<ShinobiConnector> logger, HttpClient httpClient, IOptions<ShinobiOptions> options)
|
private readonly IShinobiLinkFactory _linkFactory;
|
||||||
|
private string? _cachedVideoStream;
|
||||||
|
|
||||||
|
public ShinobiConnector(ILogger<ShinobiConnector> logger, HttpClient httpClient, IShinobiLinkFactory linkFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_options = options.Value;
|
_linkFactory = linkFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<VideoDetail>> FetchMonitorVideosBetween(DateTime from, DateTime to, bool runDry = false, CancellationToken cancellationToken = default)
|
public async Task<List<VideoDetail>> FetchMonitorVideosBetween(DateTime from, DateTime to, bool runDry = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var endpoint = $"{_options.URL}/{_options.APIKey}/videos/{_options.GroupId}/{_options.MonitorId}";
|
var endpoint = _linkFactory.BuildVideosLink(from, to);
|
||||||
endpoint += $"?start={from:yyyy-MM-ddTHH:mm:sszzz}&end={to:yyyy-MM-ddTHH:mm:sszzz}";
|
|
||||||
|
|
||||||
_logger.LogDebug("Fetching videos details from endpoint: {Endpoint}", endpoint);
|
_logger.LogDebug("Fetching videos details from endpoint: {Endpoint}", endpoint);
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ public class ShinobiConnector : IDVRConnector
|
|||||||
|
|
||||||
public async Task DownloadMonitorVideo(VideoDetail video, string downloadFolder, bool runDry, CancellationToken cancellationToken = default)
|
public async Task DownloadMonitorVideo(VideoDetail video, string downloadFolder, bool runDry, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var endpoint = $"{_options.URL}{video.href}";
|
var endpoint = _linkFactory.FromHref(video.href);
|
||||||
_logger.LogDebug("Fetching video from endpoint: {Endpoint}", endpoint);
|
_logger.LogDebug("Fetching video from endpoint: {Endpoint}", endpoint);
|
||||||
|
|
||||||
//Video filenames format: "monitorId-2025-02-15T07-45-01.mp4"
|
//Video filenames format: "monitorId-2025-02-15T07-45-01.mp4"
|
||||||
@ -85,5 +87,15 @@ public class ShinobiConnector : IDVRConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<string> GetVideoStream()
|
||||||
|
{
|
||||||
|
if (_cachedVideoStream is not null)
|
||||||
|
return _cachedVideoStream;
|
||||||
|
var endpoint = _linkFactory.BuildMonitorLink();
|
||||||
|
_logger.LogDebug("Fetching video stream from endpoint: {Endpoint}", endpoint);
|
||||||
|
var monitors = await _httpClient.GetFromJsonAsync<Monitor[]>(endpoint);
|
||||||
|
|
||||||
|
_cachedVideoStream = monitors?.FirstOrDefault()?.MonitorDetail?.auto_host ?? null;
|
||||||
|
return _cachedVideoStream ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
36
src/CasaBot/AutoScan/Implementations/ShinobiLinkFactory.cs
Normal file
36
src/CasaBot/AutoScan/Implementations/ShinobiLinkFactory.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using AutoScan.Interfaces;
|
||||||
|
using AutoScan.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace AutoScan.Implementations;
|
||||||
|
|
||||||
|
public class ShinobiLinkFactory : IShinobiLinkFactory
|
||||||
|
{
|
||||||
|
private readonly ShinobiOptions _options;
|
||||||
|
|
||||||
|
public ShinobiLinkFactory(IOptions<ShinobiOptions> options)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildMonitorLink()
|
||||||
|
{
|
||||||
|
return $"{_options.URL}/{_options.APIKey}/monitor/{_options.GroupId}/{_options.MonitorId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildFeedLink()
|
||||||
|
{
|
||||||
|
return $"{_options.URL}/{_options.APIKey}/mjpeg/{_options.GroupId}/{_options.MonitorId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildVideosLink(DateTime from, DateTime to)
|
||||||
|
{
|
||||||
|
return $"{_options.URL}/{_options.APIKey}/videos/{_options.GroupId}/{_options.MonitorId}" +
|
||||||
|
$"?start={from:yyyy-MM-ddTHH:mm:sszzz}&end={to:yyyy-MM-ddTHH:mm:sszzz}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FromHref(string href)
|
||||||
|
{
|
||||||
|
return $"{_options.URL}{href}";
|
||||||
|
}
|
||||||
|
}
|
@ -6,4 +6,6 @@ public interface IDVRConnector
|
|||||||
{
|
{
|
||||||
Task<List<VideoDetail>> FetchMonitorVideosBetween(DateTime from, DateTime to, bool runDry = false, CancellationToken cancellationToken = default);
|
Task<List<VideoDetail>> FetchMonitorVideosBetween(DateTime from, DateTime to, bool runDry = false, CancellationToken cancellationToken = default);
|
||||||
Task DownloadMonitorVideo(VideoDetail video, string downloadFolder, bool runDry = false, CancellationToken cancellationToken = default);
|
Task DownloadMonitorVideo(VideoDetail video, string downloadFolder, bool runDry = false, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<string> GetVideoStream();
|
||||||
}
|
}
|
9
src/CasaBot/AutoScan/Interfaces/IShinobiLinkFactory.cs
Normal file
9
src/CasaBot/AutoScan/Interfaces/IShinobiLinkFactory.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AutoScan.Interfaces;
|
||||||
|
|
||||||
|
public interface IShinobiLinkFactory
|
||||||
|
{
|
||||||
|
public string BuildMonitorLink();
|
||||||
|
public string BuildFeedLink();
|
||||||
|
public string BuildVideosLink(DateTime from, DateTime to);
|
||||||
|
public string FromHref(string href);
|
||||||
|
}
|
24
src/CasaBot/AutoScan/Models/Monitor.cs
Normal file
24
src/CasaBot/AutoScan/Models/Monitor.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AutoScan.Models;
|
||||||
|
|
||||||
|
public class Monitor
|
||||||
|
{
|
||||||
|
public string? ke { get; init; }
|
||||||
|
public string? name { get; init; }
|
||||||
|
public string? mid { get; init; }
|
||||||
|
public string? details { get; init; }
|
||||||
|
public string? protocol { get; init; }
|
||||||
|
public string? host { get; init; }
|
||||||
|
public int port { get; init; }
|
||||||
|
public string? path { get; init; }
|
||||||
|
|
||||||
|
public MonitorDetail? MonitorDetail => JsonSerializer.Deserialize<MonitorDetail>(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MonitorDetail
|
||||||
|
{
|
||||||
|
public string auto_host { get; set; }
|
||||||
|
public string muser { get; set; }
|
||||||
|
public string mpass { get; set; }
|
||||||
|
}
|
@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CasaBotApp", "CasaBotApp\Ca
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoScan", "AutoScan\AutoScan.csproj", "{13D75ACB-7913-4C4B-B696-9BD7383012AF}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoScan", "AutoScan\AutoScan.csproj", "{13D75ACB-7913-4C4B-B696-9BD7383012AF}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlServer", "ControlServer\ControlServer.csproj", "{11422EF5-FF40-4419-B72B-87F569E4DA3C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -18,5 +20,9 @@ Global
|
|||||||
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{13D75ACB-7913-4C4B-B696-9BD7383012AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{11422EF5-FF40-4419-B72B-87F569E4DA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{11422EF5-FF40-4419-B72B-87F569E4DA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{11422EF5-FF40-4419-B72B-87F569E4DA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{11422EF5-FF40-4419-B72B-87F569E4DA3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
||||||
@ -47,6 +48,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AutoScan\AutoScan.csproj" />
|
<ProjectReference Include="..\AutoScan\AutoScan.csproj" />
|
||||||
|
<ProjectReference Include="..\ControlServer\ControlServer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,14 +1,36 @@
|
|||||||
using AutoScan;
|
using AutoScan;
|
||||||
|
using AutoScan.Interfaces;
|
||||||
|
using CasaBotApp.TelegramBot;
|
||||||
|
using ControlServer;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
using Telegram.Bots.Types;
|
using Telegram.Bots.Types;
|
||||||
|
using BotCommand = CasaBotApp.TelegramBot.BotCommand;
|
||||||
|
|
||||||
namespace CasaBotApp.Extensions;
|
namespace CasaBotApp.Extensions;
|
||||||
|
|
||||||
public static class CommandRegister
|
public class CommandRegister
|
||||||
{
|
{
|
||||||
public static void RegisterCommands(BotHandler botHandler, AutoScanApp autoScanApp)
|
private readonly ILogger<CommandRegister> _logger;
|
||||||
|
private readonly BotHandler _botHandler;
|
||||||
|
private readonly AutoScanApp _autoScanApp;
|
||||||
|
private readonly IControlServer _controlServer;
|
||||||
|
private readonly IShinobiLinkFactory _shinobiLinkFactory;
|
||||||
|
private readonly IDVRConnector _dvrConnector;
|
||||||
|
|
||||||
|
public CommandRegister(ILogger<CommandRegister> logger, BotHandler botHandler, AutoScanApp autoScanApp, IControlServer controlServer, IShinobiLinkFactory shinobiLinkFactory, IDVRConnector dvrConnector)
|
||||||
{
|
{
|
||||||
botHandler.RegisterCommand(new BotCommand
|
_logger = logger;
|
||||||
|
_botHandler = botHandler;
|
||||||
|
_autoScanApp = autoScanApp;
|
||||||
|
_controlServer = controlServer;
|
||||||
|
_shinobiLinkFactory = shinobiLinkFactory;
|
||||||
|
_dvrConnector = dvrConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterCommands()
|
||||||
|
{
|
||||||
|
_botHandler.RegisterCommand(new BotCommand
|
||||||
{
|
{
|
||||||
Command = "/soyandre",
|
Command = "/soyandre",
|
||||||
Description = "Soy Andre",
|
Description = "Soy Andre",
|
||||||
@ -17,33 +39,63 @@ public static class CommandRegister
|
|||||||
await ctx.Responder(message, "Hola vida, te amo mucho ❤️");
|
await ctx.Responder(message, "Hola vida, te amo mucho ❤️");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
botHandler.RegisterCommand(new BotCommand
|
_botHandler.RegisterCommand(new BotCommand
|
||||||
{
|
{
|
||||||
Command = "/startScan",
|
Command = "/startScan",
|
||||||
Description = "Start a scan of last night images",
|
Description = "Start a scan of last night images",
|
||||||
Action = async (message, ctx) =>
|
Action = async (message, ctx) =>
|
||||||
{
|
{
|
||||||
await ctx.Responder(message, "Starting scan 🔍📼");
|
await ctx.Responder(message, "Starting scan 🔍📼");
|
||||||
await autoScanApp.StartNewScan();
|
await _autoScanApp.StartNewScan();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
botHandler.RegisterCommand(new BotCommand
|
_botHandler.RegisterCommand(new BotCommand
|
||||||
{
|
{
|
||||||
Command = "/lastScan",
|
Command = "/lastscan",
|
||||||
Description = "Send the images from the last scan",
|
Description = "Send the images from the last scan",
|
||||||
Action = async (message, ctx) =>
|
Action = async (message, ctx) =>
|
||||||
{
|
{
|
||||||
var images = autoScanApp.GetLastScanPictures();
|
var images = _autoScanApp.GetLastScanPictures();
|
||||||
if (images.Length == 0)
|
if (images.Length == 0)
|
||||||
{
|
{
|
||||||
await ctx.Responder(message, "No images found");
|
await ctx.Responder(message, "No images found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await botHandler.SendPhotos(message.Chat.Id, images);
|
await _botHandler.SendPhotos(message.Chat.Id, images);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
botHandler.OnReply = async msg =>
|
_botHandler.RegisterCommand(new BotCommand()
|
||||||
|
{
|
||||||
|
Command = "/now",
|
||||||
|
Description = "Send the current snapshot",
|
||||||
|
Action = async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
stopwatch.Start();
|
||||||
|
var outputPath = await TakeSnapshot();
|
||||||
|
stopwatch.Stop();
|
||||||
|
if (string.IsNullOrEmpty(outputPath))
|
||||||
|
{
|
||||||
|
await ctx.Responder(msg, "Error taking snapshot");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ = _botHandler.SendPhoto(msg.Chat.Id, outputPath, "Current snapshot");
|
||||||
|
_ = _botHandler.SendText(msg.Chat.Id, $"It took {stopwatch.ElapsedMilliseconds} ms to take the picture");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_botHandler.RegisterCommand(new BotCommand()
|
||||||
|
{
|
||||||
|
Command = "/disarm",
|
||||||
|
Description = "Disarm the Door Sensor",
|
||||||
|
Action = async (msg, ctx) =>
|
||||||
|
{
|
||||||
|
await ctx.Responder(msg, "Disarming the door sensor");
|
||||||
|
_controlServer.RequestDisarm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_botHandler.OnReply = async msg =>
|
||||||
{
|
{
|
||||||
var originalMsg = msg.ReplyToMessage;
|
var originalMsg = msg.ReplyToMessage;
|
||||||
|
|
||||||
@ -51,36 +103,100 @@ public static class CommandRegister
|
|||||||
if (originalMsg is not PhotoMessage photoMessage || photoMessage.Caption is null)
|
if (originalMsg is not PhotoMessage photoMessage || photoMessage.Caption is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var videoPath = autoScanApp.GetVideoPath(photoMessage.Caption);
|
var videoPath = _autoScanApp.GetVideoPath(photoMessage.Caption);
|
||||||
if (string.IsNullOrEmpty(videoPath))
|
if (string.IsNullOrEmpty(videoPath))
|
||||||
{
|
{
|
||||||
await botHandler.SendText(msg.Chat.Id, "No video found for this image");
|
await _botHandler.SendText(msg.Chat.Id, "No video found for this image");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await botHandler.SendVideo(msg.Chat.Id, videoPath);
|
await _botHandler.SendVideo(msg.Chat.Id, videoPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdateOnScanCompleted(BotHandler botHandler, AutoScanApp autoScanApp, ILogger logger)
|
public void RegisterAutoScanApp()
|
||||||
{
|
{
|
||||||
autoScanApp.OnScanCompleted = async () =>
|
_autoScanApp.OnScanCompleted = async () =>
|
||||||
{
|
{
|
||||||
logger.LogInformation("Scan completed at {At}", DateTime.Now);
|
_logger.LogInformation("Scan completed at {At}", DateTime.Now);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var images = autoScanApp.GetLastScanPictures();
|
var images = _autoScanApp.GetLastScanPictures();
|
||||||
if (images.Length == 0)
|
if (images.Length == 0)
|
||||||
{
|
{
|
||||||
await botHandler.UpdateText("No images found");
|
await _botHandler.UpdateText("No images found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await botHandler.UpdateText($"Scan completed, found {images.Length} images");
|
await _botHandler.UpdateText($"Scan completed, found {images.Length} images");
|
||||||
await botHandler.UpdatePhotos(images);
|
await _botHandler.UpdatePhotos(images);
|
||||||
}catch(Exception ex)
|
}catch(Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error while sending message");
|
_logger.LogError(ex, "Error while sending message");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RegisterControlServer()
|
||||||
|
{
|
||||||
|
_controlServer.OnEvent(async se =>
|
||||||
|
{
|
||||||
|
var mediaPath = await TakeSnapshot();
|
||||||
|
if (string.IsNullOrEmpty(mediaPath))
|
||||||
|
{
|
||||||
|
await _botHandler.AlertText("Unauthorized access detected 🚨🚨🚨, but no media available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (se.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 ) =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Authorizing event {EventId}", se.EventId);
|
||||||
|
_controlServer.AuthorizeEvent(se.EventId);
|
||||||
|
return Task.FromResult("Authorization not implemented");
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (se.Type == EventType.DisarmedEntrance)
|
||||||
|
{
|
||||||
|
await _botHandler.UpdateText("Authorize access");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private async Task<string> TakeSnapshot()
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
if(process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error taking snapshot, exit code: {ExitCode}", process.ExitCode);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,8 @@
|
|||||||
using AutoScan;
|
using AutoScan;
|
||||||
using AutoScan.Options;
|
using AutoScan.Options;
|
||||||
using CasaBotApp;
|
|
||||||
using CasaBotApp.Extensions;
|
using CasaBotApp.Extensions;
|
||||||
|
using CasaBotApp.TelegramBot;
|
||||||
|
using ControlServer;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -10,16 +11,16 @@ using Telegram.Bots;
|
|||||||
using Telegram.Bots.Extensions.Polling;
|
using Telegram.Bots.Extensions.Polling;
|
||||||
using Telegram.Bots.Http;
|
using Telegram.Bots.Http;
|
||||||
|
|
||||||
|
var hostBuilder = new HostBuilder();
|
||||||
|
hostBuilder.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
var environment = Environment.GetEnvironmentVariable("CASABOT_ENVIRONMENT");
|
var environment = Environment.GetEnvironmentVariable("CASABOT_ENVIRONMENT");
|
||||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.AddJsonFile($"appsettings.json", true, true)
|
.AddJsonFile($"appsettings.json", true, true)
|
||||||
.AddJsonFile($"appsettings.{environment}.json", true, true)
|
.AddJsonFile($"appsettings.{environment}.json", true, true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var hostBuilder = new HostBuilder();
|
|
||||||
hostBuilder.ConfigureServices((_, services) =>
|
|
||||||
{
|
|
||||||
services.AddSingleton(configuration);
|
services.AddSingleton(configuration);
|
||||||
|
|
||||||
services.AddLogging(configuration);
|
services.AddLogging(configuration);
|
||||||
@ -27,14 +28,20 @@ hostBuilder.ConfigureServices((_, services) =>
|
|||||||
services.Configure<TelegramOptions>(configuration.GetSection("Telegram"));
|
services.Configure<TelegramOptions>(configuration.GetSection("Telegram"));
|
||||||
services.Configure<AutoScanOptions>(configuration.GetSection("AutoScan"));
|
services.Configure<AutoScanOptions>(configuration.GetSection("AutoScan"));
|
||||||
services.Configure<ShinobiOptions>(configuration.GetSection("Shinobi"));
|
services.Configure<ShinobiOptions>(configuration.GetSection("Shinobi"));
|
||||||
|
services.Configure<ControlServerOptions>(configuration.GetSection("ControlServer"));
|
||||||
|
|
||||||
services.AddSingleton<BotHandler>();
|
|
||||||
|
services.AddSingleton<IControlServer, ControlServer.ControlServer>();
|
||||||
|
services.AddHostedService(sp => sp.GetRequiredService<IControlServer>());
|
||||||
|
|
||||||
var token = configuration["Telegram:BotToken"] ?? "";
|
var token = configuration["Telegram:BotToken"] ?? "";
|
||||||
|
services.AddSingleton<BotHandler>();
|
||||||
services.AddBotClient(token);
|
services.AddBotClient(token);
|
||||||
services.AddPolling<BotHandler>();
|
services.AddPolling<BotHandler>();
|
||||||
services.AddSingleton<IUpdateHandler>(sp => sp.GetService<BotHandler>()!);
|
services.AddSingleton<IUpdateHandler>(sp => sp.GetService<BotHandler>()!);
|
||||||
|
|
||||||
|
services.AddTransient<CommandRegister>();
|
||||||
|
|
||||||
|
|
||||||
// To get notifications when a retry is performed
|
// To get notifications when a retry is performed
|
||||||
|
|
||||||
@ -47,6 +54,7 @@ hostBuilder.ConfigureServices((_, services) =>
|
|||||||
services.Configure<HostOptions>(hostOptions =>
|
services.Configure<HostOptions>(hostOptions =>
|
||||||
{
|
{
|
||||||
hostOptions.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
|
hostOptions.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
|
||||||
|
hostOptions.ServicesStartConcurrently = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -56,17 +64,18 @@ var host = hostBuilder.Build();
|
|||||||
|
|
||||||
|
|
||||||
var logger = host.Services.GetService<ILogger<Program>>()!;
|
var logger = host.Services.GetService<ILogger<Program>>()!;
|
||||||
var botHandler = host.Services.GetService<BotHandler>()!;
|
|
||||||
var autoScanApp = host.Services.GetService<AutoScanApp>()!;
|
var autoScanApp = host.Services.GetService<AutoScanApp>()!;
|
||||||
|
|
||||||
CommandRegister.RegisterCommands(botHandler, autoScanApp);
|
var commandRegister = host.Services.GetRequiredService<CommandRegister>();
|
||||||
|
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
_ = autoScanApp.Run(cts.Token);
|
_ = autoScanApp.Run(cts.Token);
|
||||||
botHandler.Start(cts.Token);
|
|
||||||
|
|
||||||
CommandRegister.UpdateOnScanCompleted(botHandler, autoScanApp, logger);
|
commandRegister.RegisterAutoScanApp();
|
||||||
|
commandRegister.RegisterCommands();
|
||||||
|
commandRegister.RegisterControlServer();
|
||||||
|
|
||||||
logger.LogInformation("Bot started");
|
logger.LogInformation("Bot started");
|
||||||
await host.RunAsync(cts.Token);
|
await host.RunAsync(cts.Token);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System.Net.Mime;
|
|
||||||
using Telegram.Bots.Types;
|
using Telegram.Bots.Types;
|
||||||
|
|
||||||
namespace CasaBotApp;
|
namespace CasaBotApp.TelegramBot;
|
||||||
|
|
||||||
public class BotCommand
|
public class BotCommand
|
||||||
{
|
{
|
@ -1,21 +1,28 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml.Schema;
|
|
||||||
using Telegram.Bots;
|
using Telegram.Bots;
|
||||||
using Telegram.Bots.Extensions.Polling;
|
using Telegram.Bots.Extensions.Polling;
|
||||||
using Telegram.Bots.Requests.Usernames;
|
using Telegram.Bots.Requests;
|
||||||
using Telegram.Bots.Types;
|
using Telegram.Bots.Types;
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
|
using SendMediaGroup = Telegram.Bots.Requests.Usernames.SendMediaGroup;
|
||||||
|
using SendPhotoFile = Telegram.Bots.Requests.Usernames.SendPhotoFile;
|
||||||
|
using SendText = Telegram.Bots.Requests.Usernames.SendText;
|
||||||
|
using SendVideoFile = Telegram.Bots.Requests.Usernames.SendVideoFile;
|
||||||
|
|
||||||
namespace CasaBotApp;
|
namespace CasaBotApp.TelegramBot;
|
||||||
|
|
||||||
public class BotHandler : IUpdateHandler
|
public class BotHandler : IUpdateHandler
|
||||||
{
|
{
|
||||||
private readonly ILogger<BotHandler> _logger;
|
private readonly ILogger<BotHandler> _logger;
|
||||||
private readonly TelegramOptions _telegramOptions;
|
private readonly TelegramOptions _telegramOptions;
|
||||||
private readonly List<Chat> _subscribers = [];
|
private readonly List<Chat> _subscribers = [];
|
||||||
|
private readonly List<Chat> _subscribersAlarm = [];
|
||||||
private readonly Dictionary<string, BotCommand> _commands;
|
private readonly Dictionary<string, BotCommand> _commands;
|
||||||
|
//TODO hacerlo mejor.
|
||||||
|
private readonly Dictionary<string, CallbackQueueItem> _callbackFunctions = new();
|
||||||
|
private record CallbackQueueItem(DateTime inserted, Func<string, long, Task<string>> callback);
|
||||||
|
|
||||||
public Func<TextMessage, Task>? OnReply { get; set; } = null;
|
public Func<TextMessage, Task>? OnReply { get; set; } = null;
|
||||||
|
|
||||||
@ -34,6 +41,15 @@ public class BotHandler : IUpdateHandler
|
|||||||
Action = RegisterUser,
|
Action = RegisterUser,
|
||||||
Responder = Respond
|
Responder = Respond
|
||||||
});
|
});
|
||||||
|
{
|
||||||
|
RegisterCommand(new()
|
||||||
|
{
|
||||||
|
Command = "/registeralarm",
|
||||||
|
Description = "Register to receive alarms",
|
||||||
|
Action = RegisterUserAlarm,
|
||||||
|
Responder = Respond
|
||||||
|
});
|
||||||
|
}
|
||||||
RegisterCommand(new()
|
RegisterCommand(new()
|
||||||
{
|
{
|
||||||
Command = "/photo",
|
Command = "/photo",
|
||||||
@ -41,6 +57,15 @@ public class BotHandler : IUpdateHandler
|
|||||||
Action = SendImageTest,
|
Action = SendImageTest,
|
||||||
Responder = Respond
|
Responder = Respond
|
||||||
});
|
});
|
||||||
|
|
||||||
|
foreach (var subs in _telegramOptions.SubscribedChatIds)
|
||||||
|
{
|
||||||
|
Subscribe(subs);
|
||||||
|
}
|
||||||
|
foreach(var sub in _telegramOptions.SubscribedAlarmsChatIds)
|
||||||
|
{
|
||||||
|
SubscribeAlarm(sub);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterCommand(BotCommand command)
|
public void RegisterCommand(BotCommand command)
|
||||||
@ -49,13 +74,19 @@ public class BotHandler : IUpdateHandler
|
|||||||
_commands[command.Command] = command;
|
_commands[command.Command] = command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SubscribeAlarm(long id)
|
||||||
public void Start(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
foreach (var subs in _telegramOptions.SubscribedChatIds)
|
if (_subscribersAlarm.Any(x => x.Id == id))
|
||||||
{
|
{
|
||||||
Subscribe(subs);
|
_logger.LogWarning("User {Id} is already subscribed to alarm", id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_subscribersAlarm.Add(new Chat()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
_logger.LogInformation("User {Id} subscribed to receive alarms", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Subscribe(long id)
|
public void Subscribe(long id)
|
||||||
@ -77,16 +108,18 @@ public class BotHandler : IUpdateHandler
|
|||||||
/// Send text message to all subscribers
|
/// Send text message to all subscribers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public async Task UpdateText(string message)
|
public Task UpdateText(string message) => UpdateTextInt(_subscribers, message);
|
||||||
|
public Task AlertText(string message) => UpdateTextInt(_subscribersAlarm, message);
|
||||||
|
public async Task UpdateTextInt(List<Chat> subscribers,string message)
|
||||||
{
|
{
|
||||||
if (_subscribers.Count == 0)
|
if (subscribers.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No subscribers to send message to");
|
_logger.LogWarning("No subscribers to send message to");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacement = new List<(Chat from, Chat to)>();
|
var replacement = new List<(Chat from, Chat to)>();
|
||||||
foreach (var subscriber in _subscribers)
|
foreach (var subscriber in subscribers)
|
||||||
{
|
{
|
||||||
var response = await SndTxt(subscriber, message);
|
var response = await SndTxt(subscriber, message);
|
||||||
if (subscriber.FirstName is null)
|
if (subscriber.FirstName is null)
|
||||||
@ -97,8 +130,8 @@ public class BotHandler : IUpdateHandler
|
|||||||
|
|
||||||
foreach (var rep in replacement)
|
foreach (var rep in replacement)
|
||||||
{
|
{
|
||||||
_subscribers.Remove(rep.from);
|
subscribers.Remove(rep.from);
|
||||||
_subscribers.Add(rep.to);
|
subscribers.Add(rep.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,19 +140,68 @@ public class BotHandler : IUpdateHandler
|
|||||||
/// Send photo to all subscribers
|
/// Send photo to all subscribers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
public async Task UpdatePhoto(string path)
|
/// <param name="caption">Optional message with photo</param>
|
||||||
{
|
/// <param name="options">Actions options</param>
|
||||||
|
public Task UpdatePhoto(string path, string? caption = null, IEnumerable<MsgOption>? options = null) =>
|
||||||
|
UpdatePhotoInt(_subscribers, path, caption, options);
|
||||||
|
|
||||||
if (_subscribers.Count == 0)
|
public Task AlertPhoto(string path, string? caption = null, IEnumerable<MsgOption>? options = null) =>
|
||||||
|
UpdatePhotoInt(_subscribersAlarm, path, caption, options);
|
||||||
|
|
||||||
|
private async Task UpdatePhotoInt(List<Chat> subscription, string path, string? caption = null, IEnumerable<MsgOption>? options = null)
|
||||||
|
{
|
||||||
|
if (subscription.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No subscribers to send message to");
|
_logger.LogWarning("No subscribers to send message to");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subscriber in _subscribers)
|
foreach (var subscriber in subscription)
|
||||||
{
|
{
|
||||||
await using var stream = File.OpenRead(path);
|
await using var stream = File.OpenRead(path);
|
||||||
await SndPhoto(subscriber, stream);
|
ReplyMarkup? replyMarkup = null;
|
||||||
|
if (options != null)
|
||||||
|
{
|
||||||
|
foreach(var opt in options)
|
||||||
|
{
|
||||||
|
if (opt.type == OptionType.Action && opt.callback != null)
|
||||||
|
{
|
||||||
|
_callbackFunctions.Add(opt.content, new(DateTime.Now, opt.callback));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get options in enumerations of 2
|
||||||
|
var optionsGrouped = options.Select((x, i) => (x, i)).GroupBy(x => x.i / 2)
|
||||||
|
.Select(x => x.Select(y => y.x).ToArray()).ToArray();
|
||||||
|
IEnumerable<InlineButton[]> keyboard = optionsGrouped.Select(group =>
|
||||||
|
group.Select<MsgOption, InlineButton>(opt => opt.type switch
|
||||||
|
{
|
||||||
|
OptionType.Url => new UrlButton(opt.message, new Uri(opt.content)),
|
||||||
|
OptionType.Action => new CallbackDataButton(opt.message, opt.content),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(opt), opt, null)
|
||||||
|
}).ToArray()).ToArray();
|
||||||
|
|
||||||
|
replyMarkup = new InlineKeyboardMarkup(keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = new SendPhotoFile(subscriber.Id.ToString(), stream)
|
||||||
|
{
|
||||||
|
Caption = caption ?? Path.GetFileNameWithoutExtension(stream.Name),
|
||||||
|
ReplyMarkup = replyMarkup
|
||||||
|
};
|
||||||
|
await _bot.HandleAsync(send);
|
||||||
|
_ = HandleQueueExpiration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleQueueExpiration()
|
||||||
|
{
|
||||||
|
//remove expired items with more than 3 hs
|
||||||
|
var expired = _callbackFunctions.Where(x => x.Value.inserted.AddHours(3) < DateTime.Now).ToArray();
|
||||||
|
foreach (var item in expired)
|
||||||
|
{
|
||||||
|
_callbackFunctions.Remove(item.Key);
|
||||||
|
_logger.LogDebug("Removed expired callback function {Key}", item.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +280,20 @@ public class BotHandler : IUpdateHandler
|
|||||||
await _bot.HandleAsync(send);
|
await _bot.HandleAsync(send);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendPhoto(long chatId, string content, string caption)
|
||||||
|
{
|
||||||
|
await using var stream = File.OpenRead(content);
|
||||||
|
var send = new SendPhotoFile(chatId.ToString(), stream)
|
||||||
|
{
|
||||||
|
Caption = caption
|
||||||
|
};
|
||||||
|
var response = await _bot.HandleAsync(send);
|
||||||
|
if (!response.Ok)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error sending photo.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send photos to a specific chat
|
/// Send photos to a specific chat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -317,6 +413,18 @@ public class BotHandler : IUpdateHandler
|
|||||||
_logger.LogInformation("User {User} ({id}) registered to receive messages", msg.Chat.FirstName, msg.Chat.Id);
|
_logger.LogInformation("User {User} ({id}) registered to receive messages", msg.Chat.FirstName, msg.Chat.Id);
|
||||||
await Respond(msg, "You are registered to receive messages every minute");
|
await Respond(msg, "You are registered to receive messages every minute");
|
||||||
}
|
}
|
||||||
|
private async Task RegisterUserAlarm(TextMessage msg, BotCommand _)
|
||||||
|
{
|
||||||
|
if (_subscribersAlarm.Any(c => c.Id == msg.Chat.Id))
|
||||||
|
{
|
||||||
|
await Respond(msg, "You are already registered to receive Alarms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribersAlarm.Add(msg.Chat);
|
||||||
|
_logger.LogInformation("User {User} ({id}) registered to receive Alarms", msg.Chat.FirstName, msg.Chat.Id);
|
||||||
|
await Respond(msg, "You are registered to receive alarms");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private Task<Response<TextMessage>> SndTxt(long id, string txt) => _bot.HandleAsync(new SendText(id.ToString(), txt));
|
private Task<Response<TextMessage>> SndTxt(long id, string txt) => _bot.HandleAsync(new SendText(id.ToString(), txt));
|
||||||
@ -341,6 +449,9 @@ public class BotHandler : IUpdateHandler
|
|||||||
ReplyToMessageId = message.Id
|
ReplyToMessageId = message.Id
|
||||||
}, cst),
|
}, cst),
|
||||||
|
|
||||||
|
CallbackQueryUpdate u when u.Data is MessageCallbackQuery resp =>
|
||||||
|
HandleCallbackResponse(u, resp),
|
||||||
|
|
||||||
_ => Task.CompletedTask
|
_ => Task.CompletedTask
|
||||||
};
|
};
|
||||||
}catch (Exception e)
|
}catch (Exception e)
|
||||||
@ -350,5 +461,26 @@ public class BotHandler : IUpdateHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleCallbackResponse(CallbackQueryUpdate update, MessageCallbackQuery response)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Callback message received: {id} {Response}", update.Id, response.Data);
|
||||||
|
if (!_callbackFunctions.TryGetValue(response.Data, out var queueCallback))
|
||||||
|
{
|
||||||
|
_logger.LogError("Callback function not found for {Response}", response.Data);
|
||||||
|
_ = _bot.HandleAsync(new AnswerCallbackQuery(response.Id, "Option expired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var chatId = response.Message.Chat.Id;
|
||||||
|
|
||||||
|
var answer = await queueCallback.callback(response.Data, chatId);
|
||||||
|
|
||||||
|
var r = await _bot.HandleAsync(new AnswerCallbackQuery(response.Id, answer));
|
||||||
|
if (!r.Ok)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error sending callback response: {Error}", r.Failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
9
src/CasaBot/CasaBotApp/TelegramBot/MsgOption.cs
Normal file
9
src/CasaBot/CasaBotApp/TelegramBot/MsgOption.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace CasaBotApp.TelegramBot;
|
||||||
|
|
||||||
|
public record MsgOption(OptionType type, string message, string content, Func<string, long, Task<string>>? callback = null);
|
||||||
|
|
||||||
|
public enum OptionType
|
||||||
|
{
|
||||||
|
Url,
|
||||||
|
Action
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
namespace CasaBotApp;
|
namespace CasaBotApp.TelegramBot;
|
||||||
|
|
||||||
public class TelegramOptions
|
public class TelegramOptions
|
||||||
{
|
{
|
||||||
public string? BotToken { get; set; }
|
public string? BotToken { get; set; }
|
||||||
public long[] SubscribedChatIds { get; set; } = [];
|
public long[] SubscribedChatIds { get; set; } = [];
|
||||||
|
public long[] SubscribedAlarmsChatIds { get; set; } = [];
|
||||||
}
|
}
|
@ -35,5 +35,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RemoveOriginalFiles": true,
|
"RemoveOriginalFiles": true,
|
||||||
"RemoveDetectionFiles": false
|
"RemoveDetectionFiles": false,
|
||||||
|
"ControlServer": {
|
||||||
|
"port": 9003
|
||||||
|
}
|
||||||
}
|
}
|
@ -36,5 +36,8 @@
|
|||||||
},
|
},
|
||||||
"RemoveOriginalFiles": false,
|
"RemoveOriginalFiles": false,
|
||||||
"RemoveDetectionFiles": false
|
"RemoveDetectionFiles": false
|
||||||
|
},
|
||||||
|
"ControlServer": {
|
||||||
|
"port": 9003
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -87,7 +87,7 @@ time-post-event = 2s
|
|||||||
# a motion event to be triggered. Lower values require less movement, and are
|
# a motion event to be triggered. Lower values require less movement, and are
|
||||||
# more sensitive to motion. If too high, some movement may not be detected,
|
# more sensitive to motion. If too high, some movement may not be detected,
|
||||||
# while too low of a threshold can result in false detection events.
|
# while too low of a threshold can result in false detection events.
|
||||||
threshold = 2
|
threshold = 1.7
|
||||||
|
|
||||||
# Scores of this amount or higher are ignored. 255.0 is the maximum score, so
|
# Scores of this amount or higher are ignored. 255.0 is the maximum score, so
|
||||||
# values greater than 255.0 will disable the filter.
|
# values greater than 255.0 will disable the filter.
|
||||||
|
181
src/CasaBot/ControlServer/ControlServer.cs
Normal file
181
src/CasaBot/ControlServer/ControlServer.cs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ControlServer;
|
||||||
|
|
||||||
|
public class ControlServer : IControlServer
|
||||||
|
{
|
||||||
|
private readonly ILogger<ControlServer> _logger;
|
||||||
|
private readonly ControlServerOptions _options;
|
||||||
|
private readonly HttpListener _httpListener;
|
||||||
|
private readonly Dictionary<long, SensorEvent> _events = new();
|
||||||
|
private Func<SensorEvent, Task>? _onEventRecived;
|
||||||
|
private bool _disarmRequestPending;
|
||||||
|
|
||||||
|
public ControlServer(ILogger<ControlServer> logger, IOptions<ControlServerOptions> options)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_options = options.Value;
|
||||||
|
_httpListener = new HttpListener();
|
||||||
|
_httpListener.Prefixes.Add($"http://+:{_options.port}/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_httpListener.Start();
|
||||||
|
_logger.LogInformation("Http server listening on port {Port}", _options.port);
|
||||||
|
}catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error starting HTTP server: {Message}", e.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var ctx = await _httpListener.GetContextAsync();
|
||||||
|
// _logger.LogDebug("Got a request");
|
||||||
|
|
||||||
|
var request = ctx.Request;
|
||||||
|
var response = ctx.Response;
|
||||||
|
|
||||||
|
// _logger.LogInformation("Url: {Url} | Method {method}", request.Url, request.HttpMethod);
|
||||||
|
|
||||||
|
//Query params
|
||||||
|
var eId = request.QueryString["eventId"];
|
||||||
|
var eventType = request.QueryString["eventType"];
|
||||||
|
var eventData = request.QueryString["eventData"];
|
||||||
|
var eventId = long.TryParse(eId, out var id) ? id : 0;
|
||||||
|
var eventTypeEnum = Enum.TryParse<EventType>(eventType, out var type) ? type : EventType.Unknown;
|
||||||
|
var eventDataString = eventData ?? string.Empty;
|
||||||
|
var sensorEvent = new SensorEvent(eventId, eventTypeEnum, eventDataString);
|
||||||
|
|
||||||
|
|
||||||
|
if (sensorEvent.Type == EventType.Update)
|
||||||
|
{
|
||||||
|
HandleUpdate(response);
|
||||||
|
response.Close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Event Received: {event}", sensorEvent);
|
||||||
|
|
||||||
|
var notify = HandleSensorEvent(sensorEvent);
|
||||||
|
|
||||||
|
|
||||||
|
response.StatusCode = 200;
|
||||||
|
response.ContentType = "text/plain";
|
||||||
|
|
||||||
|
var message = sensorEvent.Authorization == Authorization.Unauthorized ? "UNAUTHORIZED" : "AUTHORIZE_ENTRANCE";
|
||||||
|
response.ContentLength64 = Encoding.UTF8.GetByteCount(message);
|
||||||
|
await using (var writer = new StreamWriter(response.OutputStream))
|
||||||
|
{
|
||||||
|
await writer.WriteAsync(message);
|
||||||
|
}
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
{
|
||||||
|
_ = _onEventRecived?.Invoke(sensorEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<Card> _cards = new()
|
||||||
|
{
|
||||||
|
new Card("1", "Card 1"),
|
||||||
|
new Card("2", "Card 2"),
|
||||||
|
new Card("3", "Card 3"),
|
||||||
|
new Card("4", "Card 4"),
|
||||||
|
new Card("5", "Card 5"),
|
||||||
|
};
|
||||||
|
private void HandleUpdate(HttpListenerResponse response)
|
||||||
|
{
|
||||||
|
var updateResponse = new UpdateResponse(_disarmRequestPending, _cards.ToArray());
|
||||||
|
_disarmRequestPending = false;
|
||||||
|
|
||||||
|
response.StatusCode = 200;
|
||||||
|
response.ContentType = "application/json";
|
||||||
|
// response.ContentType = "text/plain";
|
||||||
|
var message = System.Text.Json.JsonSerializer.Serialize(updateResponse);
|
||||||
|
response.ContentLength64 = Encoding.UTF8.GetByteCount(message);
|
||||||
|
using var writer = new StreamWriter(response.OutputStream);
|
||||||
|
writer.Write(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateResponse(bool disarm, Card[] cards);
|
||||||
|
public record Card(string id, string name);
|
||||||
|
private bool HandleSensorEvent(SensorEvent se)
|
||||||
|
{
|
||||||
|
if (se.Type == EventType.Update && _disarmRequestPending)
|
||||||
|
{
|
||||||
|
_disarmRequestPending = false;
|
||||||
|
se.Authorization = Authorization.Authorized;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_events.TryGetValue(se.EventId, out var storedEvent);
|
||||||
|
if (storedEvent is null)
|
||||||
|
{
|
||||||
|
//New One
|
||||||
|
if (se.Type == EventType.DisarmedEntrance)
|
||||||
|
{
|
||||||
|
_disarmRequestPending = false;
|
||||||
|
se.Authorization = Authorization.Authorized;
|
||||||
|
_events.Add(se.EventId, se);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (se.Type == EventType.Fired)
|
||||||
|
{
|
||||||
|
//Check pending desarmed.
|
||||||
|
se.Authorization = _disarmRequestPending ? Authorization.Authorized : Authorization.Unauthorized;
|
||||||
|
_disarmRequestPending = false;
|
||||||
|
_events.Add(se.EventId, se);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_events.Add(se.EventId, new SensorEvent(se.EventId, se.Type, se.Data));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Already stored
|
||||||
|
if (se.Type == EventType.EventUpdate && (_disarmRequestPending || storedEvent.Authorization == Authorization.Authorized) )
|
||||||
|
{
|
||||||
|
_disarmRequestPending = false;
|
||||||
|
storedEvent.Authorization= Authorization.Authorized;
|
||||||
|
se.Authorization = Authorization.Authorized;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// _httpListener.EndGetContext(null);
|
||||||
|
// _httpListener.Close();
|
||||||
|
_httpListener.Stop();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEvent(Func<SensorEvent, Task> onEventRecived)
|
||||||
|
{
|
||||||
|
_onEventRecived = onEventRecived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AuthorizeEvent(long eventId)
|
||||||
|
{
|
||||||
|
if (_events.TryGetValue(eventId, out var sensorEvent))
|
||||||
|
{
|
||||||
|
sensorEvent.Authorization = Authorization.Authorized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestDisarm()
|
||||||
|
{
|
||||||
|
_disarmRequestPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/CasaBot/ControlServer/ControlServer.csproj
Normal file
15
src/CasaBot/ControlServer/ControlServer.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
6
src/CasaBot/ControlServer/ControlServerOptions.cs
Normal file
6
src/CasaBot/ControlServer/ControlServerOptions.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace ControlServer;
|
||||||
|
|
||||||
|
public class ControlServerOptions
|
||||||
|
{
|
||||||
|
public int port { get; set; }
|
||||||
|
}
|
10
src/CasaBot/ControlServer/IControlServer.cs
Normal file
10
src/CasaBot/ControlServer/IControlServer.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace ControlServer;
|
||||||
|
|
||||||
|
public interface IControlServer : IHostedService
|
||||||
|
{
|
||||||
|
public void OnEvent(Func<SensorEvent, Task> onEventRecived);
|
||||||
|
public void AuthorizeEvent(long eventId);
|
||||||
|
public void RequestDisarm();
|
||||||
|
}
|
21
src/CasaBot/ControlServer/SensorEvent.cs
Normal file
21
src/CasaBot/ControlServer/SensorEvent.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace ControlServer;
|
||||||
|
|
||||||
|
public record SensorEvent(long EventId, EventType Type, string Data)
|
||||||
|
{
|
||||||
|
public Authorization Authorization { get; set; }
|
||||||
|
};
|
||||||
|
|
||||||
|
public enum EventType
|
||||||
|
{
|
||||||
|
Fired,
|
||||||
|
DisarmedEntrance,
|
||||||
|
EventUpdate,
|
||||||
|
Update,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Authorization
|
||||||
|
{
|
||||||
|
Unauthorized,
|
||||||
|
Authorized
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user