diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index 889e7df60..766b9b0e5 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -27,7 +27,7 @@ jobs: run: echo "::add-matcher::.github/matchers/dotnet.json" - name: 🛠️ Build code - run: dotnet build --configuration Release + run: dotnet build --configuration Release -p:TreatWarningsAsErrors=true - name: 👀 Unit Test run: | diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/AppThatTakesASlowTimeToDispose.cs b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/AppThatTakesASlowTimeToDispose.cs index 633addd8b..94ef22421 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/AppThatTakesASlowTimeToDispose.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/AppThatTakesASlowTimeToDispose.cs @@ -3,7 +3,7 @@ namespace LocalApps; [NetDaemonApp] -public class SlowDisposableApp : IDisposable +public sealed class SlowDisposableApp : IDisposable { public SlowDisposableApp() { diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithDisposable.cs b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithDisposable.cs index 64d667cad..95eded7de 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithDisposable.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithDisposable.cs @@ -1,7 +1,7 @@ namespace LocalApps; [NetDaemonApp] -public class MyAppLocalAppWithAsyncDispose : IAsyncDisposable, IDisposable +public sealed class MyAppLocalAppWithAsyncDispose : IAsyncDisposable, IDisposable { public bool AsyncDisposeIsCalled { get; private set; } public bool DisposeIsCalled { get; private set; } @@ -21,7 +21,7 @@ public void Dispose() } [NetDaemonApp] -public class MyAppLocalAppWithDispose : IDisposable +public sealed class MyAppLocalAppWithDispose : IDisposable { public bool DisposeIsCalled { get; private set; } @@ -30,4 +30,4 @@ public void Dispose() DisposeIsCalled = true; GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/Client/NetDaemon.HassClient.Tests/.editorconfig b/src/Client/NetDaemon.HassClient.Tests/.editorconfig index d21d08592..094409809 100644 --- a/src/Client/NetDaemon.HassClient.Tests/.editorconfig +++ b/src/Client/NetDaemon.HassClient.Tests/.editorconfig @@ -1,2 +1,4 @@ [*.cs] -dotnet_diagnostic.xUnit1030.severity = none \ No newline at end of file +dotnet_diagnostic.xUnit1030.severity = none +#Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly +dotnet_diagnostic.CA1861.severity = none diff --git a/src/Client/NetDaemon.HassClient.Tests/GlobalUsings.cs b/src/Client/NetDaemon.HassClient.Tests/GlobalUsings.cs index 44cd32ddd..eb2ec85c1 100644 --- a/src/Client/NetDaemon.HassClient.Tests/GlobalUsings.cs +++ b/src/Client/NetDaemon.HassClient.Tests/GlobalUsings.cs @@ -18,7 +18,6 @@ global using NetDaemon.Client.Internal; global using NetDaemon.Client.Internal.Helpers; global using NetDaemon.Client.Internal.Net; -global using NetDaemon.Client.Exceptions; global using NetDaemon.Client.Internal.Extensions; global using NetDaemon.Client.Internal.HomeAssistant.Commands; global using NetDaemon.Client.Settings; diff --git a/src/Client/NetDaemon.HassClient.Tests/HelperTest/ResultMessageHandlerTests.cs b/src/Client/NetDaemon.HassClient.Tests/HelperTest/ResultMessageHandlerTests.cs index 74080be07..a62944a1a 100644 --- a/src/Client/NetDaemon.HassClient.Tests/HelperTest/ResultMessageHandlerTests.cs +++ b/src/Client/NetDaemon.HassClient.Tests/HelperTest/ResultMessageHandlerTests.cs @@ -1,6 +1,6 @@ namespace NetDaemon.HassClient.Tests.HelperTest; -public class ResultMessageHandlerTests +public class ResultMessageHandlerTests : IAsyncDisposable { private readonly Mock> _loggerMock = new(); private readonly ResultMessageHandler _resultMessageHandler; @@ -84,4 +84,10 @@ private static async Task SomeUnSuccessfulResultThrowsException() await Task.Delay(400); throw new InvalidOperationException("Ohh noooo!"); } + + public async ValueTask DisposeAsync() + { + await _resultMessageHandler.DisposeAsync(); + GC.SuppressFinalize(this); + } } diff --git a/src/Client/NetDaemon.HassClient.Tests/HomeAssistantClientTest/HomeAssistantClientTests.cs b/src/Client/NetDaemon.HassClient.Tests/HomeAssistantClientTest/HomeAssistantClientTests.cs index 77f5766b5..bf1cd9c45 100644 --- a/src/Client/NetDaemon.HassClient.Tests/HomeAssistantClientTest/HomeAssistantClientTests.cs +++ b/src/Client/NetDaemon.HassClient.Tests/HomeAssistantClientTest/HomeAssistantClientTests.cs @@ -1,3 +1,4 @@ +using NetDaemon.Client.Exceptions; using NetDaemon.HassClient.Tests.Net; namespace NetDaemon.HassClient.Tests.HomeAssistantClientTest; @@ -39,7 +40,7 @@ public async Task TestConnectWithHomeAShouldReturnConnection() connection.Should().NotBeNull(); } - + [Fact] public async Task TestConnectWithOldVersionHomeAShouldReturnConnection() { @@ -111,7 +112,7 @@ private HomeAssistantClient GetDefaultAuthorizedHomeAssistantClient() ); return GetDefaultHomeAssistantClient(); } - + /// /// Return a pre authenticated and running state /// HomeAssistantClient @@ -143,7 +144,7 @@ private HomeAssistantClient GetDefaultConnectOkHomeAssistantClient() Success = true } //{"id":1,"type":"result","success":true,"result":null} ); - + // The add the fake config state that says running _haConnectionMock.AddConfigResponseMessage( new HassConfig @@ -152,8 +153,8 @@ private HomeAssistantClient GetDefaultConnectOkHomeAssistantClient() } ); return GetDefaultHomeAssistantClient(); - } - + } + /// /// Return a pre authenticated and running state /// HomeAssistantClient with version less than 2023.9.x diff --git a/src/Client/NetDaemon.HassClient.Tests/HomeAssistantRunnerTest/HomeAssistantRunnerTests.cs b/src/Client/NetDaemon.HassClient.Tests/HomeAssistantRunnerTest/HomeAssistantRunnerTests.cs index 38d2b09b2..7f8215949 100644 --- a/src/Client/NetDaemon.HassClient.Tests/HomeAssistantRunnerTest/HomeAssistantRunnerTests.cs +++ b/src/Client/NetDaemon.HassClient.Tests/HomeAssistantRunnerTest/HomeAssistantRunnerTests.cs @@ -1,3 +1,4 @@ +using NetDaemon.Client.Exceptions; using NNetDaemon.HassClient.Tests.HomeAssistantRunnerTest; namespace NetDaemon.HassClient.Tests.HomeAssistantRunnerTest; @@ -33,7 +34,7 @@ public async Task TestSuccessfulShouldPostConnection() DefaultRunner.CurrentConnection.Should().NotBeNull(); try { - cancelSource.Cancel(); + await cancelSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) @@ -74,7 +75,7 @@ public async Task TestUnSuccessfulConnectionShouldPostCorrectDisconnectError() try { - cancelSource.Cancel(); + await cancelSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) @@ -114,7 +115,7 @@ public async Task TestNotReadyConnectionShouldPostCorrectDisconnectError() try { - cancelSource.Cancel(); + await cancelSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) @@ -154,7 +155,7 @@ public async Task TestNotAuthorizedConnectionShouldPostCorrectDisconnectError() try { - cancelSource.Cancel(); + await cancelSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) @@ -180,7 +181,7 @@ public async Task TestClientDisconnectShouldPostCorrectDisconnectError() DefaultRunner.RunAsync("host", 0, false, "token", "wspath", TimeSpan.FromMilliseconds(100), cancelSource.Token); // await DefaultRunner.DisposeAsync().ConfigureAwait(false); - cancelSource.Cancel(); + await cancelSource.CancelAsync(); var reason = await disconnectionTask.ConfigureAwait(false); DefaultRunner.CurrentConnection.Should().BeNull(); try @@ -222,7 +223,7 @@ public async Task TestRemoteDisconnectShouldPostCorrectDisconnectError() var reason = await disconnectionTask.ConfigureAwait(false); try { - cancelSource.Cancel(); + await cancelSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/src/Client/NetDaemon.HassClient.Tests/Integration/HomeAssistantServerMock.cs b/src/Client/NetDaemon.HassClient.Tests/Integration/HomeAssistantServerMock.cs index 4c85f9fcf..64ec6014b 100644 --- a/src/Client/NetDaemon.HassClient.Tests/Integration/HomeAssistantServerMock.cs +++ b/src/Client/NetDaemon.HassClient.Tests/Integration/HomeAssistantServerMock.cs @@ -7,7 +7,7 @@ namespace NetDaemon.HassClient.Tests.Integration; /// The Home Assistant Mock class implements a fake Home Assistant server by /// exposing the websocket api and fakes responses to requests. /// -public class HomeAssistantMock : IAsyncDisposable +public sealed class HomeAssistantMock : IAsyncDisposable { public const int RecieiveBufferSize = 1024 * 4; public IHost HomeAssistantHost { get; } @@ -30,7 +30,6 @@ public HomeAssistantMock() public async ValueTask DisposeAsync() { await Stop().ConfigureAwait(false); - GC.SuppressFinalize(this); } /// @@ -63,12 +62,13 @@ private async Task Stop() await HomeAssistantHost.StopAsync().ConfigureAwait(false); await HomeAssistantHost.WaitForShutdownAsync().ConfigureAwait(false); } + } /// /// The class implementing the mock hass server /// -public class HassMockStartup : IHostedService +public sealed class HassMockStartup : IHostedService, IDisposable { private readonly byte[] _authOkMessage = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Integration", "Testdata", "auth_ok.json")); @@ -89,10 +89,9 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { - _cancelSource.Cancel(); - return Task.CompletedTask; + await _cancelSource.CancelAsync(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment e) @@ -430,4 +429,9 @@ private sealed class SendCommandMessage [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; [JsonPropertyName("id")] public int Id { get; set; } = 0; } + + public void Dispose() + { + _cancelSource.Dispose(); + } } diff --git a/src/Client/NetDaemon.HassClient.Tests/Integration/IntegrationTestBase.cs b/src/Client/NetDaemon.HassClient.Tests/Integration/IntegrationTestBase.cs index 2b12ba880..2ae0a53d7 100644 --- a/src/Client/NetDaemon.HassClient.Tests/Integration/IntegrationTestBase.cs +++ b/src/Client/NetDaemon.HassClient.Tests/Integration/IntegrationTestBase.cs @@ -2,7 +2,7 @@ namespace NetDaemon.HassClient.Tests.Integration; public class IntegrationTestBase : IClassFixture { - protected readonly CancellationTokenSource TokenSource = new(TestSettings.DefaultTimeout); + protected CancellationTokenSource TokenSource { get; } = new(TestSettings.DefaultTimeout); protected IntegrationTestBase(HomeAssistantServiceFixture fixture) { diff --git a/src/Client/NetDaemon.HassClient.Tests/Integration/WebsocketIntegrationTests.cs b/src/Client/NetDaemon.HassClient.Tests/Integration/WebsocketIntegrationTests.cs index 608f8a299..a91519704 100644 --- a/src/Client/NetDaemon.HassClient.Tests/Integration/WebsocketIntegrationTests.cs +++ b/src/Client/NetDaemon.HassClient.Tests/Integration/WebsocketIntegrationTests.cs @@ -1,3 +1,5 @@ +using NetDaemon.Client.Exceptions; + namespace NetDaemon.HassClient.Tests.Integration; public class WebsocketIntegrationTests : IntegrationTestBase @@ -337,7 +339,7 @@ public async Task TestMultipleSubscribeEventChangeAndGetEvent() .Should() .NotBeNull(); - secondChangedEvent!.NewState!.Attributes!.FirstOrDefault(n => n.Key == "battery_level")! + secondChangedEvent.NewState!.Attributes!.FirstOrDefault(n => n.Key == "battery_level")! .Should() .NotBeNull(); } diff --git a/src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantApiCallException.cs b/src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantApiCallException.cs index a270fd895..b7041ac41 100644 --- a/src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantApiCallException.cs +++ b/src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantApiCallException.cs @@ -1,6 +1,6 @@ using System.Net; -namespace NetDaemon.Client.Internal.Exceptions; +namespace NetDaemon.Client.Exceptions; [SuppressMessage("", "RCS1194")] public class HomeAssistantApiCallException : Exception diff --git a/src/Client/NetDaemon.HassClient/GlobalUsings.cs b/src/Client/NetDaemon.HassClient/GlobalUsings.cs index ecde64c45..36035b7a3 100644 --- a/src/Client/NetDaemon.HassClient/GlobalUsings.cs +++ b/src/Client/NetDaemon.HassClient/GlobalUsings.cs @@ -24,7 +24,6 @@ global using NetDaemon.Client.Internal.Helpers; global using NetDaemon.Client.Internal.Json; global using NetDaemon.Client.Internal.Net; -global using NetDaemon.Client.Exceptions; global using NetDaemon.Client.Internal.Extensions; global using NetDaemon.Client.Internal.HomeAssistant.Commands; global using NetDaemon.Client.Internal.HomeAssistant.Messages; diff --git a/src/Client/NetDaemon.HassClient/Internal/Helpers/AsyncLazy.cs b/src/Client/NetDaemon.HassClient/Internal/Helpers/AsyncLazy.cs index 13aa08fbb..c1d4fd703 100644 --- a/src/Client/NetDaemon.HassClient/Internal/Helpers/AsyncLazy.cs +++ b/src/Client/NetDaemon.HassClient/Internal/Helpers/AsyncLazy.cs @@ -1,9 +1,4 @@ namespace NetDaemon.Client.Internal.Helpers; -public class AsyncLazy : Lazy> -{ - public AsyncLazy(Func> taskFactory) : - base(() => Task.Factory.StartNew(taskFactory).Unwrap()) - { - } -} \ No newline at end of file +public class AsyncLazy(Func> taskFactory) : Lazy>(() + => Task.Factory.StartNew(taskFactory).Unwrap()); diff --git a/src/Client/NetDaemon.HassClient/Internal/Helpers/ResultMessageHandler.cs b/src/Client/NetDaemon.HassClient/Internal/Helpers/ResultMessageHandler.cs index af824d938..69c8f8a51 100644 --- a/src/Client/NetDaemon.HassClient/Internal/Helpers/ResultMessageHandler.cs +++ b/src/Client/NetDaemon.HassClient/Internal/Helpers/ResultMessageHandler.cs @@ -2,17 +2,11 @@ namespace NetDaemon.Client.Internal.Helpers; -internal class ResultMessageHandler : IAsyncDisposable +internal class ResultMessageHandler(ILogger logger) : IAsyncDisposable { internal int WaitForResultTimeout = 20000; private readonly CancellationTokenSource _tokenSource = new(); private readonly ConcurrentDictionary, object?> _backgroundTasks = new(); - private readonly ILogger _logger; - - public ResultMessageHandler(ILogger logger) - { - _logger = logger; - } public void HandleResult(Task returnMessageTask, CommandMessage originalCommand) { @@ -33,7 +27,7 @@ async Task Wrap() if (awaitedTask != task) { // We have a timeout - _logger.LogWarning( + logger.LogWarning( "Command ({CommandType}) did not get response in timely fashion. Sent command is {CommandMessage}", command.Type, command); } @@ -42,14 +36,14 @@ async Task Wrap() var result = await task.ConfigureAwait(false); if (!result.Success ?? false) { - _logger.LogWarning( + logger.LogWarning( "Failed command ({CommandType}) error: {ErrorResult}. Sent command is {CommandMessage}", command.Type, result.Error, command); } } catch (Exception e) { - _logger.LogError(e, "Exception waiting for result message Sent command is {CommandMessage}", command); + logger.LogError(e, "Exception waiting for result message Sent command is {CommandMessage}", command); } finally { diff --git a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantApiManager.cs b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantApiManager.cs index b84935796..11b928c5b 100644 --- a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantApiManager.cs +++ b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantApiManager.cs @@ -1,4 +1,4 @@ -using NetDaemon.Client.Internal.Exceptions; +using NetDaemon.Client.Exceptions; namespace NetDaemon.Client.Internal; @@ -82,4 +82,4 @@ private static string GetApiUrl(HomeAssistantSettings settings) var httpScheme = settings.Ssl ? "https" : "http"; return $"{httpScheme}://{settings.Host}:{settings.Port}/api"; } -} \ No newline at end of file +} diff --git a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantClient.cs b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantClient.cs index e5c0dd427..435532e90 100644 --- a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantClient.cs +++ b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantClient.cs @@ -1,3 +1,5 @@ +using NetDaemon.Client.Exceptions; + namespace NetDaemon.Client.Internal; internal class HomeAssistantClient(ILogger logger, diff --git a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs index 74387b397..60396f2d4 100644 --- a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs +++ b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs @@ -1,3 +1,5 @@ +using NetDaemon.Client.Exceptions; + namespace NetDaemon.Client.Internal; internal class HomeAssistantRunner(IHomeAssistantClient client, diff --git a/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs b/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs index 50d39df48..c17d6fc27 100644 --- a/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs +++ b/src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipeline.cs @@ -1,6 +1,6 @@ namespace NetDaemon.Client.Internal.Net; -internal class WebSocketClientTransportPipeline : IWebSocketClientTransportPipeline +internal class WebSocketClientTransportPipeline(IWebSocketClient clientWebSocket) : IWebSocketClientTransportPipeline { /// /// Default Json serialization options, Hass expects intended @@ -13,12 +13,7 @@ internal class WebSocketClientTransportPipeline : IWebSocketClientTransportPipel private readonly CancellationTokenSource _internalCancelSource = new(); private readonly Pipe _pipe = new(); - private readonly IWebSocketClient _ws; - - public WebSocketClientTransportPipeline(IWebSocketClient clientWebSocket) - { - _ws = clientWebSocket ?? throw new ArgumentNullException(nameof(clientWebSocket)); - } + private readonly IWebSocketClient _ws = clientWebSocket ?? throw new ArgumentNullException(nameof(clientWebSocket)); private static int DefaultTimeOut => 5000; diff --git a/src/Extensions/NetDaemon.Extensions.Tts/Internal/TextToSpeechService.cs b/src/Extensions/NetDaemon.Extensions.Tts/Internal/TextToSpeechService.cs index 65211b621..826967257 100644 --- a/src/Extensions/NetDaemon.Extensions.Tts/Internal/TextToSpeechService.cs +++ b/src/Extensions/NetDaemon.Extensions.Tts/Internal/TextToSpeechService.cs @@ -51,7 +51,7 @@ private async Task ProcessTtsMessages() await homeAssistantConnection .CallServiceAsync("tts", ttsMessage.Service, data, hassTarget, _cancellationTokenSource.Token) .ConfigureAwait(false); - // Wait for media player to report state + // Wait for media player to report state await Task.Delay(InternalTimeForTtsDelay, _cancellationTokenSource.Token).ConfigureAwait(false); var state = await homeAssistantConnection .GetEntityStateAsync(ttsMessage.EntityId, _cancellationTokenSource.Token).ConfigureAwait(false); @@ -83,9 +83,9 @@ await homeAssistantConnection public async ValueTask DisposeAsync() { - _cancellationTokenSource.Cancel(); + await _cancellationTokenSource.CancelAsync(); await _processTtsTask.ConfigureAwait(false); _cancellationTokenSource.Dispose(); } - -} \ No newline at end of file + +} diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/.editorconfig b/src/HassModel/NetDaemon.HassModel.CodeGenerator/.editorconfig new file mode 100644 index 000000000..fee9b68ab --- /dev/null +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/.editorconfig @@ -0,0 +1,7 @@ + [*.cs] +# Warning CA1862 : Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862) +dotnet_diagnostic.CA1862.severity = none +# Warning CA1860 : Prefer comparing 'Count' to 0 rather than using 'Any()' +dotnet_diagnostic.CA1860.severity = none +# Warning CA1869 : Avoid creating a new 'JsonSerializerOptions' instance for every +dotnet_diagnostic.CA1869.severity = none diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs index 324816a9d..9c00449fc 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs @@ -14,7 +14,7 @@ namespace NetDaemon.HassModel.CodeGenerator; /// { /// target.CallService("press"); /// } -/// +/// /// ///Press the input button entity. /// public static void Press(this IEnumerable target) /// { @@ -28,7 +28,7 @@ internal static class ExtensionMethodsGenerator public static IEnumerable Generate(IEnumerable serviceDomains, IReadOnlyCollection entityDomains) { var entityClassNameByDomain = entityDomains.ToLookup(e => e.Domain, e => e.CoreInterfaceName ?? e.EntityClassName); - + return serviceDomains .Select(sd => GenerateDomainExtensionClass(sd, entityClassNameByDomain)) .OfType(); // filter out nulls @@ -41,7 +41,7 @@ public static IEnumerable Generate(IEnumerable GenerateExtensionMethodsForService(serviceDomain.Domain, service, entityClassNameByDomain)) .ToArray(); - if (!serviceMethodDeclarations.Any()) return null; + if (serviceMethodDeclarations.Length == 0) return null; return ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain)) .AddMembers(serviceMethodDeclarations) @@ -51,9 +51,9 @@ public static IEnumerable Generate(IEnumerable GenerateExtensionMethodsForService(string domain, HassService service, ILookup entityClassNameByDomain) { - // There can be multiple Target Domains, so generate methods for each + // There can be multiple Target Domains, so generate methods for each var targetEntityDomains = service.Target?.Entity.SelectMany(e => e.Domain) ?? Array.Empty(); - + return targetEntityDomains.SelectMany(targetEntityDomain => GenerateExtensionMethodsForService(domain, service, targetEntityDomain, entityClassNameByDomain)); } @@ -92,7 +92,7 @@ private static MemberDeclarationSyntax ExtensionMethodWithoutArguments(HassServi """)! .WithSummaryComment(service.Description); } - + private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments) { return ParseMemberDeclaration($$""" @@ -103,7 +103,7 @@ private static MemberDeclarationSyntax ExtensionMethodWithClassArgument(HassServ """)! .WithSummaryComment(service.Description); } - + private static MemberDeclarationSyntax ExtensionMethodWithSeparateArguments(HassService service, string serviceName, string entityTypeName, ServiceArguments serviceArguments) { return ParseMemberDeclaration($$""" diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ServicesGenerator.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ServicesGenerator.cs index c48bb36c9..e0bb5cee4 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ServicesGenerator.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ServicesGenerator.cs @@ -13,7 +13,7 @@ public static IEnumerable Generate(IReadOnlyList sd.Services?.Any() == true).GroupBy(x => x.Domain, x => x.Services)) + foreach (var domainServicesGroup in serviceDomains.Where(sd => sd.Services.Any() == true).GroupBy(x => x.Domain, x => x.Services)) { var domain = domainServicesGroup.Key!; var domainServices = domainServicesGroup @@ -110,7 +110,7 @@ private static IEnumerable GenerateServiceMethod(string } else { - // method using arguments object + // method using arguments object yield return ParseMemberDeclaration($$""" void {{serviceMethodName}}({{JoinList(targetParam, serviceArguments.TypeName)}} data) { @@ -121,7 +121,7 @@ private static IEnumerable GenerateServiceMethod(string .WithSummaryComment(service.Description) .AppendTrivia(targetComment); - // method using arguments as separate parameters + // method using arguments as separate parameters yield return ParseMemberDeclaration($$""" void {{serviceMethodName}}({{JoinList(targetParam, serviceArguments.GetParametersList())}}) { @@ -136,4 +136,4 @@ private static IEnumerable GenerateServiceMethod(string } private static string JoinList(params string?[] args) => string.Join(", ", args.Where(s => !string.IsNullOrEmpty(s))); -} \ No newline at end of file +} diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Controller.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Controller.cs index 6fcf274a5..0af1f319c 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Controller.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Controller.cs @@ -8,32 +8,24 @@ namespace NetDaemon.HassModel.CodeGenerator; #pragma warning disable CA1303 #pragma warning disable CA2000 // because of await using ... configureAwait() -internal class Controller +internal class Controller(CodeGenerationSettings generationSettings, HomeAssistantSettings haSettings) { private const string ResourceName = "NetDaemon.HassModel.CodeGenerator.MetaData.DefaultMetadata.DefaultEntityMetaData.json"; - private readonly CodeGenerationSettings _generationSettings; - private readonly HomeAssistantSettings _haSettings; - - public Controller(CodeGenerationSettings generationSettings, HomeAssistantSettings haSettings) - { - _generationSettings = generationSettings; - _haSettings = haSettings; - } private string EntityMetaDataFileName => Path.Combine(OutputFolder, "EntityMetaData.json"); private string ServicesMetaDataFileName => Path.Combine(OutputFolder, "ServicesMetaData.json"); - private string OutputFolder => string.IsNullOrEmpty(_generationSettings.OutputFolder) - ? Directory.GetParent(Path.GetFullPath(_generationSettings.OutputFile))!.FullName - : _generationSettings.OutputFolder; + private string OutputFolder => string.IsNullOrEmpty(generationSettings.OutputFolder) + ? Directory.GetParent(Path.GetFullPath(generationSettings.OutputFile))!.FullName + : generationSettings.OutputFolder; public async Task RunAsync() { - var (hassStates, servicesMetaData) = await HaRepositry.GetHaData(_haSettings).ConfigureAwait(false); + var (hassStates, servicesMetaData) = await HaRepositry.GetHaData(haSettings).ConfigureAwait(false); var previousEntityMetadata = await LoadEntitiesMetaDataAsync().ConfigureAwait(false); var currentEntityMetaData = EntityMetaDataGenerator.GetEntityDomainMetaData(hassStates); - var mergedEntityMetaData = EntityMetaDataMerger.Merge(_generationSettings, previousEntityMetadata, currentEntityMetaData); + var mergedEntityMetaData = EntityMetaDataMerger.Merge(generationSettings, previousEntityMetadata, currentEntityMetaData); await Save(mergedEntityMetaData, EntityMetaDataFileName).ConfigureAwait(false); await Save(servicesMetaData, ServicesMetaDataFileName).ConfigureAwait(false); @@ -107,17 +99,17 @@ private async Task Save(T merged, string fileName) private void SaveGeneratedCode(MemberDeclarationSyntax[] generatedTypes) { - if (!_generationSettings.GenerateOneFilePerEntity) + if (!generationSettings.GenerateOneFilePerEntity) { Console.WriteLine("Generating single file for all entities."); - var unit = Generator.BuildCompilationUnit(_generationSettings.Namespace, generatedTypes.ToArray()); + var unit = Generator.BuildCompilationUnit(generationSettings.Namespace, generatedTypes.ToArray()); - Directory.CreateDirectory(Directory.GetParent(_generationSettings.OutputFile)!.FullName); + Directory.CreateDirectory(Directory.GetParent(generationSettings.OutputFile)!.FullName); - using var writer = new StreamWriter(_generationSettings.OutputFile); + using var writer = new StreamWriter(generationSettings.OutputFile); unit.WriteTo(writer); - Console.WriteLine(Path.GetFullPath(_generationSettings.OutputFile)); + Console.WriteLine(Path.GetFullPath(generationSettings.OutputFile)); } else { @@ -127,7 +119,7 @@ private void SaveGeneratedCode(MemberDeclarationSyntax[] generatedTypes) foreach (var type in generatedTypes) { - var unit = Generator.BuildCompilationUnit(_generationSettings.Namespace, type); + var unit = Generator.BuildCompilationUnit(generationSettings.Namespace, type); using var writer = new StreamWriter(Path.Combine(OutputFolder, $"{unit.GetClassName()}.cs")); unit.WriteTo(writer); } diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/EntityIdHelper.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/EntityIdHelper.cs index a868d5880..8232861d4 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/EntityIdHelper.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/EntityIdHelper.cs @@ -7,11 +7,11 @@ internal static class EntityIdHelper public static string GetDomain(string str) { - return str[..str.IndexOf(".", StringComparison.InvariantCultureIgnoreCase)]; + return str[..str.IndexOf('.', StringComparison.InvariantCultureIgnoreCase)]; } public static string GetEntity(string str) { - return str[(str.IndexOf(".", StringComparison.InvariantCultureIgnoreCase) + 1)..]; + return str[(str.IndexOf('.', StringComparison.InvariantCultureIgnoreCase) + 1)..]; } -} \ No newline at end of file +} diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsDoubleConverter.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsDoubleConverter.cs index 3cac5bca9..acf9977ff 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsDoubleConverter.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsDoubleConverter.cs @@ -15,7 +15,7 @@ class StringAsDoubleConverter : JsonConverter }; } - double? Skip(ref Utf8JsonReader reader) + static double? Skip(ref Utf8JsonReader reader) { reader.Skip(); return null; diff --git a/src/HassModel/NetDaemon.HassModel.Tests/.editorconfig b/src/HassModel/NetDaemon.HassModel.Tests/.editorconfig index d21d08592..21f1ec4d1 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/.editorconfig +++ b/src/HassModel/NetDaemon.HassModel.Tests/.editorconfig @@ -1,2 +1,5 @@ [*.cs] -dotnet_diagnostic.xUnit1030.severity = none \ No newline at end of file +dotnet_diagnostic.xUnit1030.severity = none +# Warning CA1861 : Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861) +dotnet_diagnostic.CA1861.severity= none + diff --git a/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs index f33b8b9e7..decac9b8f 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs @@ -11,7 +11,7 @@ namespace NetDaemon.HassModel.Tests.Internal; -public class AppScopedHaContextProviderTest +public sealed class AppScopedHaContextProviderTest : IDisposable { private readonly Mock _hassConnectionMock = new(); private readonly Subject _hassEventSubjectMock = new(); @@ -278,4 +278,10 @@ private async Task CreateServiceProvider() } public record TestEventData(string command, int endpoint_id, string otherField); + + public void Dispose() + { + _hassEventSubjectMock.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/HassModel/NetDaemon.HassModel.Tests/Internal/BackgroundTaskTrackerTests.cs b/src/HassModel/NetDaemon.HassModel.Tests/Internal/BackgroundTaskTrackerTests.cs index 32847885d..877a6a4f7 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/Internal/BackgroundTaskTrackerTests.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/Internal/BackgroundTaskTrackerTests.cs @@ -4,7 +4,7 @@ namespace NetDaemon.HassModel.Tests.Internal; -public class BackgroundTaskTrackerTests +public sealed class BackgroundTaskTrackerTests : IAsyncDisposable { private readonly BackgroundTaskTracker _backgroundTaskTracker; private readonly Mock> _loggerMock = new(); @@ -66,4 +66,9 @@ async Task CallMeAndIThrowError() It.IsAny(), It.Is>((_, _) => true)!), Times.Once); } + + public async ValueTask DisposeAsync() + { + await _backgroundTaskTracker.DisposeAsync(); + } } diff --git a/src/HassModel/NetDaemon.HassModel.Tests/Internal/TriggerManagerTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/Internal/TriggerManagerTest.cs index 028fd76eb..505af86c1 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/Internal/TriggerManagerTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/Internal/TriggerManagerTest.cs @@ -12,7 +12,7 @@ namespace NetDaemon.HassModel.Tests.Internal; -public class TriggerManagerTest +public sealed class TriggerManagerTest : IDisposable { private readonly ITriggerManager _triggerManager; @@ -48,14 +48,14 @@ private ServiceProvider CreateServiceProvider() serviceCollection.AddScopedHaContext(); var haRunnerMock = new Mock(); - + haRunnerMock.SetupGet(n => n.CurrentConnection).Returns(_hassConnectionMock.Object); serviceCollection.AddSingleton(_ => haRunnerMock.Object); var provider = serviceCollection.BuildServiceProvider(); return provider; - } + } [Fact] @@ -67,13 +67,13 @@ public async Task RegisterTrigger() _messageSubject.OnNext(new HassMessage(){Id = nextMessageId, Event = new HassEvent(){Variables = new HassVariable() {TriggerElement = message }}}); - + // Assert await incomingTriggersTask.Should() .CompleteWithinAsync(TimeSpan.FromSeconds(1), "the message should have arrived by now") .WithResult(message); } - + [Fact] public async Task NoMoreTriggersAfterDispose() { @@ -81,19 +81,19 @@ public async Task NoMoreTriggersAfterDispose() var incomingTriggersTask = _triggerManager.RegisterTrigger(new {}).FirstAsync().ToTask().ToFunc(); await ((IAsyncDisposable)_triggerManager).DisposeAsync(); - - // Assert, Dispose should unsubscribe with HA AND stop any messages that do pass - + + // Assert, Dispose should unsubscribe with HA AND stop any messages that do pass + _messageSubject.OnNext(new HassMessage(){Id = nextMessageId, Event = new HassEvent(){Variables = new HassVariable() {TriggerElement = new JsonElement() }}}); - + await incomingTriggersTask.Should() .NotCompleteWithinAsync(TimeSpan.FromSeconds(1), "no messages should arrive after being disposed"); - + _hassConnectionMock.Verify(m => m.SendCommandAndReturnHassMessageResponseAsync( new UnsubscribeEventsCommand(nextMessageId), It.IsAny())); - } - + } + [Fact] public async Task RegisterTriggerCorrectMessagesPerSubscription() @@ -103,24 +103,29 @@ public async Task RegisterTriggerCorrectMessagesPerSubscription() nextMessageId = 7; var incomingTriggersTask7 = _triggerManager.RegisterTrigger(new {}).FirstAsync().ToTask().ToFunc(); - + var message6 = new { tag = "six" }.AsJsonElement(); var message7 = new { tag = "seven" }.AsJsonElement(); _messageSubject.OnNext(new HassMessage{Id = 6, Event = new HassEvent(){Variables = new HassVariable() {TriggerElement = message6 }}}); - + _messageSubject.OnNext(new HassMessage{Id = 7, Event = new HassEvent(){Variables = new HassVariable() {TriggerElement = message7 }}}); - + // Assert await incomingTriggersTask6.Should() .CompleteWithinAsync(TimeSpan.FromSeconds(1), $"{nameof(message6)} should have arrived by now") .WithResult(message6); - + await incomingTriggersTask7.Should() .CompleteWithinAsync(TimeSpan.FromSeconds(1), $"{nameof(message7)} should have arrived by now") .WithResult(message7); } + + public void Dispose() + { + _messageSubject.Dispose(); + } } diff --git a/src/HassModel/NetDaemon.HassModel.Tests/StateObservableExtensionsTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/StateObservableExtensionsTest.cs index afa3c0a50..61d5c615b 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/StateObservableExtensionsTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/StateObservableExtensionsTest.cs @@ -6,10 +6,10 @@ namespace NetDaemon.HassModel.Tests; -public class StateObservableExtensionsTest +public sealed class StateObservableExtensionsTest : IDisposable { private readonly Subject _subject = new(); - + private readonly TestScheduler _testScheduler = new(); private readonly IObservable _numericStateChangeObservable; @@ -38,8 +38,8 @@ public void WhenNumStateIsForFiresInTime() eventTimes.Verify(m => m.OnNext(50), Times.Once); // event should be fired at 10 ticks after 40 eventTimes.VerifyNoOtherCalls(); - } - + } + [Fact] public void WhenNumericStateIsForFiresInTime() { @@ -51,18 +51,23 @@ public void WhenNumericStateIsForFiresInTime() TriggerStateChange(tick: 30, "19", "21"); // this does start the timer TriggerStateChange(tick: 35, "21", "15"); // this stops the timer before it reaches 10 TriggerStateChange(tick: 40, "15", "22"); // this starts the timer again - TriggerStateChange(tick: 45, "22", "25"); // this event should not stop the timer + TriggerStateChange(tick: 45, "22", "25"); // this event should not stop the timer _testScheduler.AdvanceTo(100); eventTimes.Verify(m => m.OnNext(50), Times.Once); // event should be fired at 10 ticks after 40 eventTimes.VerifyNoOtherCalls(); - - } - + + } + private void TriggerStateChange(long tick, string old, string @new) { _testScheduler.AdvanceTo(tick); _subject.OnNext(new StateChange(new Entity(Mock.Of(), "Dummy"), new EntityState { State = old }, new EntityState { State = @new })); } -} \ No newline at end of file + + public void Dispose() + { + _subject.Dispose(); + } +} diff --git a/src/HassModel/NetDeamon.HassModel/DependencyInjectionSetup.cs b/src/HassModel/NetDeamon.HassModel/DependencyInjectionSetup.cs index ad92dec3b..62b23f4c8 100644 --- a/src/HassModel/NetDeamon.HassModel/DependencyInjectionSetup.cs +++ b/src/HassModel/NetDeamon.HassModel/DependencyInjectionSetup.cs @@ -5,7 +5,7 @@ namespace NetDaemon.HassModel; /// -/// Setup methods for services configuration +/// Setup methods for services configuration /// public static class DependencyInjectionSetup { @@ -14,7 +14,8 @@ public static class DependencyInjectionSetup /// public static IHostBuilder UseAppScopedHaContext(this IHostBuilder hostBuilder) { - if (hostBuilder == null) throw new ArgumentNullException(nameof(hostBuilder)); + + ArgumentNullException.ThrowIfNull(hostBuilder, nameof(hostBuilder)); return hostBuilder .ConfigureServices((_, services) => services.AddScopedHaContext()); diff --git a/src/HassModel/NetDeamon.HassModel/HaContextExtensions.cs b/src/HassModel/NetDeamon.HassModel/HaContextExtensions.cs index a725d3c82..ab0abf037 100644 --- a/src/HassModel/NetDeamon.HassModel/HaContextExtensions.cs +++ b/src/HassModel/NetDeamon.HassModel/HaContextExtensions.cs @@ -13,7 +13,7 @@ public static class HaContextExtensions /// public static IObservable StateChanges(this IHaContext haContext) { - if (haContext == null) throw new ArgumentNullException(nameof(haContext)); + ArgumentNullException.ThrowIfNull(haContext, nameof(haContext)); return haContext.StateAllChanges().StateChangesOnly(); } @@ -21,7 +21,7 @@ public static IObservable StateChanges(this IHaContext haContext) /// Creates a new Entity instance /// public static Entity Entity(this IHaContext haContext, string entityId) => new (haContext, entityId); - + /// /// Filters events on their EventType and retrieves their data in a types object /// @@ -34,4 +34,4 @@ public static IObservable> Filter(this IObservable events, st => events .Where(e => e.EventType == eventType && e.DataElement != null) .Select(e => new Event(e)); -} \ No newline at end of file +} diff --git a/src/HassModel/NetDeamon.HassModel/Internal/AppScopedHaContextProvider.cs b/src/HassModel/NetDeamon.HassModel/Internal/AppScopedHaContextProvider.cs index ce796d0fd..28653d168 100644 --- a/src/HassModel/NetDeamon.HassModel/Internal/AppScopedHaContextProvider.cs +++ b/src/HassModel/NetDeamon.HassModel/Internal/AppScopedHaContextProvider.cs @@ -71,7 +71,7 @@ public void CallService(string domain, string service, ServiceTarget? target = n .ConfigureAwait(false); return result?.Response; } - + public IObservable StateAllChanges() { return _queuedObservable.Where(n => @@ -89,12 +89,10 @@ public void SendEvent(string eventType, object? data = null) _backgroundTaskTracker.TrackBackgroundTask(_apiManager.SendEventAsync(eventType, _tokenSource.Token, data), "Error in sending event"); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { if (!_tokenSource.IsCancellationRequested) - _tokenSource.Cancel(); + await _tokenSource.CancelAsync(); _tokenSource.Dispose(); - - return ValueTask.CompletedTask; } } diff --git a/src/HassModel/NetDeamon.HassModel/StateObservableExtensions.cs b/src/HassModel/NetDeamon.HassModel/StateObservableExtensions.cs index aa2abc78c..2c97f445c 100644 --- a/src/HassModel/NetDeamon.HassModel/StateObservableExtensions.cs +++ b/src/HassModel/NetDeamon.HassModel/StateObservableExtensions.cs @@ -12,7 +12,7 @@ public static class StateObservableExtensions /// [Obsolete("Use the overload with IScheduler instead")] public static IObservable WhenStateIsFor( - this IObservable observable, + this IObservable observable, Func predicate, TimeSpan timeSpan) => observable.WhenStateIsFor(predicate, timeSpan, Scheduler.Default); @@ -22,26 +22,26 @@ public static IObservable WhenStateIsFor( /// [Obsolete("Use the overload with IScheduler instead")] public static IObservable> WhenStateIsFor( - this IObservable> observable, - Func predicate, + this IObservable> observable, + Func predicate, TimeSpan timeSpan) where TEntity : Entity - where TEntityState : EntityState + where TEntityState : EntityState => observable.WhenStateIsFor(predicate, timeSpan, Scheduler.Default); - + /// /// Waits for an EntityState to match a predicate for the specified time /// public static IObservable WhenStateIsFor( - this IObservable observable, + this IObservable observable, Func predicate, TimeSpan timeSpan, IScheduler scheduler) { - if (observable == null) throw new ArgumentNullException(nameof(observable)); - if (predicate == null) throw new ArgumentNullException(nameof(predicate)); - if (scheduler == null) throw new ArgumentNullException(nameof(scheduler)); - + ArgumentNullException.ThrowIfNull(observable, nameof(observable)); + ArgumentNullException.ThrowIfNull(predicate, nameof(predicate)); + ArgumentNullException.ThrowIfNull(scheduler, nameof(scheduler)); + return observable // Only process changes that start or stop matching the predicate .Where(e => predicate(e.Old) != predicate(e.New)) @@ -57,17 +57,17 @@ public static IObservable WhenStateIsFor( /// Waits for an EntityState to match a predicate for the specified time /// public static IObservable> WhenStateIsFor( - this IObservable> observable, - Func predicate, + this IObservable> observable, + Func predicate, TimeSpan timeSpan, IScheduler scheduler) where TEntity : Entity where TEntityState : EntityState { - if (observable == null) throw new ArgumentNullException(nameof(observable)); - if (predicate == null) throw new ArgumentNullException(nameof(predicate)); - if (scheduler == null) throw new ArgumentNullException(nameof(scheduler)); - + ArgumentNullException.ThrowIfNull(observable, nameof(observable)); + ArgumentNullException.ThrowIfNull(predicate, nameof(predicate)); + ArgumentNullException.ThrowIfNull(scheduler, nameof(scheduler)); + return observable .Where(e => predicate(e.Old) != predicate(e.New)) .Throttle(timeSpan, scheduler) diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs index 10d315d45..8592280ed 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs @@ -30,7 +30,7 @@ public async Task TestApplicationIsLoaded() var instances = service.ApplicationInstances; instances.Where(n => n.Id == "LocalApps.LocalApp").Should().NotBeEmpty(); - timedCancellationSource.Cancel(); + await timedCancellationSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } @@ -103,7 +103,7 @@ public async Task TestApplicationReactToNewEventsAndThrowException() State = "on" }); - timedCancellationSource.Cancel(); + await timedCancellationSource.CancelAsync(); await runnerTask.ConfigureAwait(false); } diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs index f97c5d04a..c6d000291 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NetDaemon.AppModel; -using NetDaemon.Client.Internal.Exceptions; +using NetDaemon.Client.Exceptions; using NetDaemon.HassModel; using NetDaemon.Runtime.Internal; diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateRepositoryTests.cs b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateRepositoryTests.cs index f006b32b2..1343062bd 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateRepositoryTests.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateRepositoryTests.cs @@ -1,6 +1,6 @@ using System.Net; using Microsoft.Extensions.Hosting; -using NetDaemon.Client.Internal.Exceptions; +using NetDaemon.Client.Exceptions; using NetDaemon.Runtime.Internal; namespace NetDaemon.Runtime.Tests.Internal; diff --git a/src/Runtime/NetDaemon.Runtime/Internal/AppStateRepository.cs b/src/Runtime/NetDaemon.Runtime/Internal/AppStateRepository.cs index bd24e30b3..b464c0a14 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/AppStateRepository.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/AppStateRepository.cs @@ -1,6 +1,6 @@ using System.Net; +using NetDaemon.Client.Exceptions; using NetDaemon.Client.HomeAssistant.Model; -using NetDaemon.Client.Internal.Exceptions; namespace NetDaemon.Runtime.Internal; diff --git a/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs b/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs index 2e43230e6..fadd0d3aa 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs @@ -19,7 +19,7 @@ internal class NetDaemonRuntime(IHomeAssistantRunner homeAssistantRunner, private IAppModelContext? _applicationModelContext; private CancellationToken? _stoppingToken; - private CancellationTokenSource? _runnerCancelationSource; + private CancellationTokenSource? _runnerCancellationSource; public bool IsConnected; @@ -45,7 +45,7 @@ public async Task StartAsync(CancellationToken stoppingToken) .Subscribe(); try { - _runnerCancelationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + _runnerCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); _runnerTask = homeAssistantRunner.RunAsync( _haSettings.Host, @@ -54,7 +54,7 @@ public async Task StartAsync(CancellationToken stoppingToken) _haSettings.Token, _haSettings.WebsocketPath, TimeSpan.FromSeconds(TimeoutInSeconds), - _runnerCancelationSource.Token); + _runnerCancellationSource.Token); // Make sure we only return after the connection is made and initialization is ready await _startedAndConnected.Task; @@ -165,11 +165,13 @@ public async ValueTask DisposeAsync() _isDisposed = true; await DisposeApplicationsAsync().ConfigureAwait(false); - _runnerCancelationSource?.Cancel(); + if (_runnerCancellationSource is not null) + await _runnerCancellationSource.CancelAsync(); try { await _runnerTask.ConfigureAwait(false); } catch (OperationCanceledException) { } + _runnerCancellationSource?.Dispose(); } } diff --git a/src/debug/DebugHost/apps/Client/.editorconfig b/src/debug/DebugHost/apps/Client/.editorconfig new file mode 100644 index 000000000..033f1079e --- /dev/null +++ b/src/debug/DebugHost/apps/Client/.editorconfig @@ -0,0 +1,3 @@ +[*.cs] +#Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly +dotnet_diagnostic.CA1861.severity = none \ No newline at end of file diff --git a/src/debug/DebugHost/apps/Client/ClientDebug.cs b/src/debug/DebugHost/apps/Client/ClientDebug.cs index f5e3bfa21..7bbb00047 100644 --- a/src/debug/DebugHost/apps/Client/ClientDebug.cs +++ b/src/debug/DebugHost/apps/Client/ClientDebug.cs @@ -20,23 +20,23 @@ public ClientApp(ILogger logger, ITriggerManager triggerManager) new { platform = "state", - entity_id = new string[] { "media_player.vardagsrum" }, - from = new string[] { "idle", "playing" }, + entity_id = new[] { "media_player.vardagsrum" }, + from = new[] { "idle", "playing" }, to = "off" }); - triggerObservable.Subscribe(n => + triggerObservable.Subscribe(n => _logger.LogCritical("Got trigger message: {Message}", n) ); - + var timePatternTriggerObservable = triggerManager.RegisterTrigger(new { platform = "time_pattern", id = "some id", seconds = "/1" }); - - var disposedSubscription = timePatternTriggerObservable.Subscribe(n => + + var disposedSubscription = timePatternTriggerObservable.Subscribe(n => _logger.LogCritical("Got trigger message: {Message}", n) ); } @@ -46,7 +46,7 @@ public ValueTask DisposeAsync() _logger.LogInformation("disposed app"); return ValueTask.CompletedTask; } - + record TimePatternResult(string id, string alias, string platform, DateTimeOffset now, string description); }