From b585840b4cce79befa6d4305d06aadd8437b22e4 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Wed, 4 Sep 2024 14:57:36 -0500 Subject: [PATCH] Electronics TIme Updates: * Send admin password from config to client * Only trigger status updates to the client if something changes * Send client status to Home Assistant * Misc code cleanup * Update NuGet's --- .editorconfig | 1 + NuttyTree.NetDaemon.sln | 1 + .../ElectronicsTime/ElectronicsTimeApp.cs | 4 +- .../Options/ElectronicsTimeOptions.cs | 8 ++- .../gRPC/ElectronicsTimeGrpcService.cs | 54 +++++++++++++++++-- .../gRPC/electronics_time.proto | 3 +- .../NuttyTree.NetDaemon.Application.csproj | 14 ++--- .../IHomeAssistantWebhookApi.cs | 9 ++++ .../IServiceCollectionExtensions.cs | 22 ++++++++ .../NuttyTree.NetDaemon.Infrastructure.csproj | 2 +- .../NuttyTree.NetDaemon.csproj | 2 +- ...ree.NetDaemon.Application.UnitTests.csproj | 6 +-- 12 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IHomeAssistantWebhookApi.cs create mode 100644 src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IServiceCollectionExtensions.cs diff --git a/.editorconfig b/.editorconfig index 52cbbaf..2950090 100644 --- a/.editorconfig +++ b/.editorconfig @@ -315,6 +315,7 @@ csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_primary_constructors = true:suggestion +dotnet_diagnostic.SA1010.severity = none [/src/*Controller.cs] diff --git a/NuttyTree.NetDaemon.sln b/NuttyTree.NetDaemon.sln index e7b336d..8a1f4f6 100644 --- a/NuttyTree.NetDaemon.sln +++ b/NuttyTree.NetDaemon.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1E435BC4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{08A2828C-5FD1-4917-B92E-A65EB0BC182C}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props build\Dockerfile = build\Dockerfile EndProjectSection diff --git a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/ElectronicsTimeApp.cs b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/ElectronicsTimeApp.cs index 59f4e4c..62bfcfc 100644 --- a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/ElectronicsTimeApp.cs +++ b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/ElectronicsTimeApp.cs @@ -36,9 +36,9 @@ internal sealed class ElectronicsTimeApp : IDisposable private readonly string? chrisUserId; - private readonly List toDoListUpdateTriggers = new List(); + private readonly List toDoListUpdateTriggers = []; - private readonly List taskTriggers = new List(); + private readonly List taskTriggers = []; private readonly ITriggerableTask updateToDoListTask; diff --git a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/Options/ElectronicsTimeOptions.cs b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/Options/ElectronicsTimeOptions.cs index 52fc1ec..45f9b7c 100644 --- a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/Options/ElectronicsTimeOptions.cs +++ b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/Options/ElectronicsTimeOptions.cs @@ -4,7 +4,11 @@ namespace NuttyTree.NetDaemon.Application.ElectronicsTime.Options; internal sealed class ElectronicsTimeOptions { - public IList ToDoListItems { get; set; } = new List(); + public IList ToDoListItems { get; set; } = []; - public IList Applications { get; set; } = new List(); + public string AdminPassword { get; set; } = string.Empty; + + public string WebhookId { get; set; } = string.Empty; + + public IList Applications { get; set; } = []; } diff --git a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/ElectronicsTimeGrpcService.cs b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/ElectronicsTimeGrpcService.cs index 9fb8838..0b40358 100644 --- a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/ElectronicsTimeGrpcService.cs +++ b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/ElectronicsTimeGrpcService.cs @@ -1,6 +1,8 @@ -using Grpc.Core; +using FluentDateTime; +using Grpc.Core; using Microsoft.Extensions.Options; using NuttyTree.NetDaemon.Application.ElectronicsTime.Options; +using NuttyTree.NetDaemon.ExternalServices.HomeAssistantWebhook; using NuttyTree.NetDaemon.Infrastructure.Extensions; using NuttyTree.NetDaemon.Infrastructure.HomeAssistant; @@ -12,10 +14,16 @@ internal sealed class ElectronicsTimeGrpcService : ElectronicsTimeGrpc.Electroni private readonly IEntities homeAssistantEntities; - public ElectronicsTimeGrpcService(IOptionsMonitor options, IEntities homeAssistantEntities) + private readonly IHomeAssistantWebhookApi homeAssistantWebhook; + + public ElectronicsTimeGrpcService( + IOptionsMonitor options, + IEntities homeAssistantEntities, + IHomeAssistantWebhookApi homeAssistantWebhook) { this.options = options; this.homeAssistantEntities = homeAssistantEntities; + this.homeAssistantWebhook = homeAssistantWebhook; } public override async Task GetApplicationConfig(ApplicationConfigRequest request, IServerStreamWriter responseStream, ServerCallContext context) @@ -27,6 +35,7 @@ public override async Task GetApplicationConfig(ApplicationConfigRequest request { var response = new ApplicationConfigResponse { + AdminPassword = options.CurrentValue.AdminPassword, Applications = { options.CurrentValue.Applications.Select(a => new Application @@ -51,10 +60,16 @@ public override async Task GetApplicationConfig(ApplicationConfigRequest request public override async Task GetStatus(StatusRequest request, IServerStreamWriter responseStream, ServerCallContext context) { var updateStatusTrigger = new TaskCompletionSource(); - using var timer = new Timer(_ => updateStatusTrigger.TrySetResult(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + using var modeChange = homeAssistantEntities.InputSelect.MaysonElectronicsMode.StateChanges().Subscribe(_ => updateStatusTrigger.TrySetResult()); + using var locationChange = homeAssistantEntities.DeviceTracker.PhoneMayson.StateChanges().Subscribe(_ => updateStatusTrigger.TrySetResult()); + using var availableTimeChange = homeAssistantEntities.Sensor.MaysonAvailableTime.StateChanges().Subscribe(_ => updateStatusTrigger.TrySetResult()); + using var tasksChange = homeAssistantEntities.Todo.Mayson.StateChanges().Subscribe(_ => updateStatusTrigger.TrySetResult()); while (!context.CancellationToken.IsCancellationRequested) { + // We have to recreate the timer each time because the time to next daytime change can vary based on Daylight Saving Time + using var daytimeChange = new Timer(_ => updateStatusTrigger.TrySetResult(), null, GetTimeToNextDaytimeChange(), TimeSpan.MaxValue); + var response = new StatusResponse { Mode = homeAssistantEntities.InputSelect.MaysonElectronicsMode.EntityState.AsEnum() ?? ElectronicsMode.Restricted, @@ -70,8 +85,37 @@ public override async Task GetStatus(StatusRequest request, IServerStreamWriter< } } - public override Task SendDeviceStatus(DeviceStatus request, ServerCallContext context) + public override async Task SendDeviceStatus(DeviceStatus request, ServerCallContext context) + { + await homeAssistantWebhook.CallWebhookAsync( + options.CurrentValue.WebhookId, + new + { + request.CurrentApp, + request.CurrentPipApp, + IsUsingTime = options.CurrentValue.Applications.Any(a => a.RequiresTime && (a.Name == request.CurrentApp || a.Name == request.CurrentPipApp)), + }, + context.CancellationToken); + + return new DeviceStatusResponse(); + } + + private TimeSpan GetTimeToNextDaytimeChange() { - return Task.FromResult(new DeviceStatusResponse()); + var now = DateTime.Now; + + var morning = now.At(8, 0); + if (now <= morning) + { + return morning - now; + } + + var evening = now.At(21, 0); + if (now <= evening) + { + return evening - now; + } + + return morning.NextDay() - now; } } diff --git a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/electronics_time.proto b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/electronics_time.proto index fc5b085..f00de09 100644 --- a/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/electronics_time.proto +++ b/src/NuttyTree.NetDaemon.Application/ElectronicsTime/gRPC/electronics_time.proto @@ -14,7 +14,8 @@ message ApplicationConfigRequest { } message ApplicationConfigResponse { - repeated Application applications = 1; + string adminPassword = 1; + repeated Application applications = 2; } message Application { diff --git a/src/NuttyTree.NetDaemon.Application/NuttyTree.NetDaemon.Application.csproj b/src/NuttyTree.NetDaemon.Application/NuttyTree.NetDaemon.Application.csproj index 1b8e371..8eca955 100644 --- a/src/NuttyTree.NetDaemon.Application/NuttyTree.NetDaemon.Application.csproj +++ b/src/NuttyTree.NetDaemon.Application/NuttyTree.NetDaemon.Application.csproj @@ -9,18 +9,18 @@ - - - - - - + + + + + + - + diff --git a/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IHomeAssistantWebhookApi.cs b/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IHomeAssistantWebhookApi.cs new file mode 100644 index 0000000..3aea1ee --- /dev/null +++ b/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IHomeAssistantWebhookApi.cs @@ -0,0 +1,9 @@ +using Refit; + +namespace NuttyTree.NetDaemon.ExternalServices.HomeAssistantWebhook; + +public interface IHomeAssistantWebhookApi +{ + [Put("/api/webhook/{webhookId}")] + Task CallWebhookAsync(string webhookId, [Body] object data, CancellationToken cancellationToken = default); +} diff --git a/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IServiceCollectionExtensions.cs b/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..87d9263 --- /dev/null +++ b/src/NuttyTree.NetDaemon.ExternalServices/HomeAssistantWebhook/IServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NetDaemon.Client.Settings; +using Refit; + +namespace NuttyTree.NetDaemon.ExternalServices.HomeAssistantWebhook; + +public static class IServiceCollectionExtensions +{ + public static IServiceCollection AddHomeAssistantWebhooks(this IServiceCollection services) + { + services.AddRefitClient() + .AddDefaultRetryPolicy() + .ConfigureHttpClient((serviceProvider, client) => + { + var settings = serviceProvider.GetRequiredService>().Value; + client.BaseAddress = new UriBuilder(settings.Ssl ? "https" : "http", settings.Host, settings.Port).Uri; + }); + + return services; + } +} diff --git a/src/NuttyTree.NetDaemon.Infrastructure/NuttyTree.NetDaemon.Infrastructure.csproj b/src/NuttyTree.NetDaemon.Infrastructure/NuttyTree.NetDaemon.Infrastructure.csproj index cb418c3..61578f2 100644 --- a/src/NuttyTree.NetDaemon.Infrastructure/NuttyTree.NetDaemon.Infrastructure.csproj +++ b/src/NuttyTree.NetDaemon.Infrastructure/NuttyTree.NetDaemon.Infrastructure.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/NuttyTree.NetDaemon/NuttyTree.NetDaemon.csproj b/src/NuttyTree.NetDaemon/NuttyTree.NetDaemon.csproj index 9f24095..936f032 100644 --- a/src/NuttyTree.NetDaemon/NuttyTree.NetDaemon.csproj +++ b/src/NuttyTree.NetDaemon/NuttyTree.NetDaemon.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/NuttyTree.NetDaemon.Application.UnitTests/NuttyTree.NetDaemon.Application.UnitTests.csproj b/tests/NuttyTree.NetDaemon.Application.UnitTests/NuttyTree.NetDaemon.Application.UnitTests.csproj index 4a9d1bb..740c646 100644 --- a/tests/NuttyTree.NetDaemon.Application.UnitTests/NuttyTree.NetDaemon.Application.UnitTests.csproj +++ b/tests/NuttyTree.NetDaemon.Application.UnitTests/NuttyTree.NetDaemon.Application.UnitTests.csproj @@ -6,9 +6,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive