├── .github └── workflows │ ├── dotnet.yml │ ├── example_build_test.yml │ └── release_main.yml ├── .gitignore ├── HaKafkaNet.sln ├── LICENSE ├── README.md ├── example ├── HaKafkaNet.ExampleApp.Tests │ ├── Automations │ │ └── AutomationWithPreStartupTests.cs │ ├── GlobalUsings.cs │ ├── HaKafkaNet.ExampleApp.Tests.csproj │ ├── HaKafkanetFixture.cs │ └── IntegrationTests │ │ ├── ActiveTests.cs │ │ ├── LightOnRegistryTests.cs │ │ └── SmokeTests.cs ├── HaKafkaNet.ExampleApp │ ├── Automations │ │ ├── AdvancedTutorialRegistry.cs │ │ ├── AutomationWithPreStartup.cs │ │ ├── ConditionalAutomationExample.cs │ │ ├── ExampleDurableAutomation.cs │ │ ├── ExampleDurableAutomation2.cs │ │ ├── ExceptionTrowingAutomation.cs │ │ ├── LightOnCustomAutomation.cs │ │ ├── LightOnRegistry.cs │ │ ├── MotionBehaviorTutorial.cs │ │ ├── SceneControllerAutomation.cs │ │ ├── SimpleLightAutomation.cs │ │ ├── TemplateRegistry.cs │ │ └── UpdatingEntityRegistry.cs │ ├── Dockerfile │ ├── HaKafkaNet.ExampleApp.csproj │ ├── Models │ │ └── AutoGen.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SystemMonitorExample.cs │ ├── TestClasses │ │ ├── ActiveRegistry.cs │ │ ├── SmokeTestRegistry.cs │ │ └── readme.md │ └── appsettings.json └── docker-compose.yml ├── images ├── HKN Social media banner.png ├── HaKafkaNetDashboard.png ├── HaKafkaNetDashboardV4.png ├── UI Examples │ ├── AutomationDetail-V5_1.PNG │ ├── AutomationDetail-V5_2.PNG │ ├── AutomationDetail-expanded-V5_3.PNG │ ├── Dashboard-Detail-expanded-V5_2.PNG │ ├── Dashboard-V5_1.PNG │ ├── Dashboard-V5_2.PNG │ ├── Dashboard-V5_5.PNG │ ├── LogDetails.PNG │ └── Menu-V5_5.PNG ├── hkn.png ├── hkn_064.png ├── hkn_128.png ├── hkn_256.png └── hkn_512.png ├── infrastructure ├── docker-compose.yml ├── hakafkanet.jinja └── hakafkanet.yaml ├── package-lock.json └── src ├── HaKafkaNet.Tests ├── GlobalUsings.cs ├── HaKafkaNet.Tests.csproj ├── Implementations │ ├── AutomationManagerTests │ │ ├── GetAllTests.cs │ │ ├── GetByKeyTests.cs │ │ └── GetByTriggerIdTests.cs │ ├── Automations │ │ ├── AutomationWrapperTests.cs │ │ ├── ConditionalAutomationWrapperTests.cs │ │ ├── SchedulableAutomationWrapperTests.cs │ │ └── SunComponentTests.cs │ ├── HaEntityProviderTests.cs │ └── Models │ │ └── HaEntityStateConversionTests.cs ├── KafkaHandlers │ └── HaStateHandlerComponentTests.cs ├── Models │ └── StateChangeExtensionTests.cs └── TestHelpers.cs ├── HaKafkaNet.UI ├── .env ├── .env.development ├── .env.production ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── hkn_128.png │ │ └── icons │ │ │ └── bootstrap-icons.svg │ ├── components │ │ ├── AutomationDetails.tsx │ │ ├── AutomationList.tsx │ │ ├── AutomationListItem.tsx │ │ ├── ErrorLogs.tsx │ │ ├── HknHeaderFooter.tsx │ │ ├── LogEntry.tsx │ │ └── TraceItem.tsx │ ├── main.tsx │ ├── models │ │ ├── AutomationData.ts │ │ ├── AutomationDetailResponse.ts │ │ └── SystemInfo.ts │ ├── services │ │ └── Api.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── HaKafkaNet ├── API ├── ApiResponse.cs ├── GetAutomationDetails │ ├── AutomationEndpoint.cs │ └── AutomationResponse.cs ├── GetAutomations │ ├── AutomationListResponse.cs │ └── GetAutomationListEndpoint.cs ├── GetErrorLog │ └── GetErrorLogEndpoint.cs ├── GetSystemInfo │ ├── GetSystemInfoEndpoint.cs │ └── SystemInfoResponse.cs ├── Notifications │ └── NotificationEndpoint.cs ├── NotifyStartupShutdown │ └── NotifyStartupShutdownEndpoint.cs └── PostEnableAutomation │ └── EnableAutomationEndpoint.cs ├── Controllers └── HaKafkaNetController.cs ├── DI ├── ExcludeFromDiscoveryAttribute.cs ├── OtelExtensions.cs └── ServicesExtensions.cs ├── HaKafkaNet.csproj ├── HaKafkaNet.sln ├── Implementations ├── AutomationBuilder │ ├── AutomationBuilder.cs │ ├── AutomationBuilderException.cs │ ├── AutomationBuilderExtensions.cs │ ├── AutomationBuildingInfo.cs │ ├── DelayableExtensions.cs │ └── PrebuiltExtensions.cs ├── Automations │ ├── AutomationExtensions.cs │ ├── AutomationFactory.cs │ ├── BaseAutomations │ │ ├── ConditionalAutomation.cs │ │ ├── DelayableAutomationBase.cs │ │ ├── SchedulableAutomation.cs │ │ ├── SimpleAutomation.cs │ │ └── TypedAutomation.cs │ ├── Prebuilt │ │ ├── LightOffOnNoMotion.cs │ │ ├── LightOnMotionAutomation.cs │ │ └── SunAutomations.cs │ └── Wrappers │ │ ├── AutomationWrapper.cs │ │ ├── DelayableAutomationWrapper.cs │ │ ├── Executors │ │ └── IAutomationExecutor.cs │ │ ├── IAutomationWrapper.cs │ │ ├── TypedAutomationWrapper.cs │ │ ├── TypedDelayedAutomationWrapper.cs │ │ └── WrapperFactory.cs ├── Core │ ├── AutomationActivator.cs │ ├── AutomationManager.cs │ ├── AutomationRegistrar.cs │ ├── HknLogTarget.cs │ ├── StartUpShutDownEventExtensions.cs │ ├── SystemObserver.cs │ ├── TraceLogProvider.cs │ └── UpdatingEntityProvider.cs ├── Services │ ├── EntityStateProviderExtensions.cs │ ├── HaApiExtensions.cs │ ├── HaApiProvider.cs │ ├── HaEntityProvider.cs │ ├── HaServices.cs │ └── HaStateCache.cs └── StartupHelpers.cs ├── KafkaHandlers ├── HaMessageResolver.cs └── HaStateHandler.cs ├── Models ├── AutomationMetaData.cs ├── BadEntityState.cs ├── EntityModels │ ├── BaseEntityModel.cs │ ├── CalendarModel.cs │ ├── ClimateEnums.cs │ ├── CommonEnums.cs │ ├── GeoLocation.cs │ ├── HaAutomationModel.cs │ ├── HaEntittyState.cs │ ├── HaEntityStateChange.cs │ ├── LightModel.cs │ ├── LightProps.cs │ ├── Lock.cs │ ├── MediaPlayer.cs │ ├── SceneControllerEvent.cs │ ├── StateAware │ │ └── ThreadSafeEntity.cs │ ├── StateChangeHelperExtensions.cs │ ├── StateChangeTransformations.cs │ ├── StateExtensions.cs │ ├── Sun.cs │ ├── SunExtensions.cs │ ├── TagAttributes.cs │ └── Weather.cs ├── EventTiming.cs ├── HaApiModels │ ├── Bytes.cs │ ├── HaNotification.cs │ ├── LightTurnOnModel.cs │ ├── NotificationAction.cs │ ├── NotificationCommand.cs │ ├── PiperSettings.cs │ └── RokuCommands.cs ├── HaKafkaNetConfig.cs ├── HaKafkaNetException.cs ├── JsonConverters │ ├── GlobalConverters.cs │ ├── HaByteGroupingConverters.cs │ └── HaDateTimeConverter.cs ├── TraceData.cs └── TraceEvent.cs ├── PublicInterfaces ├── AuxiliaryAutomationInterfaces.cs ├── IAutomation.cs ├── IAutomationBuilder.cs ├── IAutomationFactory.cs ├── IAutomationRegistry.cs ├── IHaApiProvider.cs ├── IHaEntityProvider.cs ├── IHaServices.cs ├── IHaStateCache.cs ├── IStartupHelpers.cs ├── IStrongTypedAutomations.cs ├── ISystemMonitor.cs └── IUpdatingEntityProvider.cs ├── Testing ├── ServicesTestExtensions.cs └── TestHelper.cs ├── nugetAssets ├── hkn_128.png └── readme.md └── www ├── assets ├── bootstrap-icons-sb4yxpby.svg ├── hkn_128-f6PbFPlS.png ├── index-B0ACNvIN.css └── index-CwC-c-b1.js ├── favicon.ico └── index.html /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main", "debug" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 9.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | working-directory: ./src/HaKafkaNet 26 | - name: Build 27 | run: dotnet build 28 | working-directory: ./src/HaKafkaNet 29 | - name: Unit Tests 30 | run: dotnet test ./HaKafkaNet.Tests 31 | working-directory: ./src 32 | - name: Integration Tests 33 | run: dotnet test ./HaKafkaNet.ExampleApp.Tests 34 | working-directory: ./example 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/example_build_test.yml: -------------------------------------------------------------------------------- 1 | name: built and test example app 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup .NET 12 | uses: actions/setup-dotnet@v4 13 | with: 14 | dotnet-version: 8.x 15 | - name: Restore dependencies 16 | run: dotnet restore 17 | working-directory: ./example/HaKafkaNet.ExampleApp 18 | - name: Build 19 | run: dotnet build 20 | working-directory: ./example/HaKafkaNet.ExampleApp 21 | - name: Test 22 | run: dotnet test 23 | working-directory: ./src/HaKafkaNet.ExampleApp.Tests 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/release_main.yml: -------------------------------------------------------------------------------- 1 | name: publish HaKafkaNet to nuget 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: build, pack & publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup .NET SDK 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: 9.x 18 | - name: Build 19 | run: dotnet build -c Release 20 | working-directory: ./src/HaKafkaNet 21 | - name: Test 22 | run: dotnet test -c Release --no-build 23 | working-directory: ./src/HaKafkaNet 24 | - name: Pack nugets 25 | run: dotnet pack -c Release --no-build --output . 26 | working-directory: ./src/HaKafkaNet 27 | - name: Push to NuGet 28 | run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET}} --source https://api.nuget.org/v3/index.json 29 | working-directory: ./src/HaKafkaNet 30 | -------------------------------------------------------------------------------- /HaKafkaNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84B25554-403E-456F-83CD-1A938C3BCE31}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaKafkaNet", "src\HaKafkaNet\HaKafkaNet.csproj", "{4A8D3D34-E08D-4A72-90DF-F356E4A21FAD}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{97387C27-9CF9-45B9-9DC5-FE96DAB0A63F}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HaKafkaNet.ExampleApp", "example\HaKafkaNet.ExampleApp\HaKafkaNet.ExampleApp.csproj", "{5A49C63C-01F4-47C0-8B1D-801B37EA77A9}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HaKafkaNet.ExampleApp.Tests", "example\HaKafkaNet.ExampleApp.Tests\HaKafkaNet.ExampleApp.Tests.csproj", "{F8D4457F-325A-4944-88BC-DF3E06063C76}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HaKafkaNet.Tests", "src\HaKafkaNet.Tests\HaKafkaNet.Tests.csproj", "{698471FF-514B-46E3-B5D0-CF602AB1B4A1}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {4A8D3D34-E08D-4A72-90DF-F356E4A21FAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {4A8D3D34-E08D-4A72-90DF-F356E4A21FAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {4A8D3D34-E08D-4A72-90DF-F356E4A21FAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {4A8D3D34-E08D-4A72-90DF-F356E4A21FAD}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5A49C63C-01F4-47C0-8B1D-801B37EA77A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5A49C63C-01F4-47C0-8B1D-801B37EA77A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5A49C63C-01F4-47C0-8B1D-801B37EA77A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5A49C63C-01F4-47C0-8B1D-801B37EA77A9}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {F8D4457F-325A-4944-88BC-DF3E06063C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {F8D4457F-325A-4944-88BC-DF3E06063C76}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {F8D4457F-325A-4944-88BC-DF3E06063C76}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {F8D4457F-325A-4944-88BC-DF3E06063C76}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {698471FF-514B-46E3-B5D0-CF602AB1B4A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {698471FF-514B-46E3-B5D0-CF602AB1B4A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {698471FF-514B-46E3-B5D0-CF602AB1B4A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {698471FF-514B-46E3-B5D0-CF602AB1B4A1}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {4A8D3D34-E08D-4A72-90DF-F356E4A21FAD} = {84B25554-403E-456F-83CD-1A938C3BCE31} 46 | {5A49C63C-01F4-47C0-8B1D-801B37EA77A9} = {97387C27-9CF9-45B9-9DC5-FE96DAB0A63F} 47 | {F8D4457F-325A-4944-88BC-DF3E06063C76} = {97387C27-9CF9-45B9-9DC5-FE96DAB0A63F} 48 | {698471FF-514B-46E3-B5D0-CF602AB1B4A1} = {84B25554-403E-456F-83CD-1A938C3BCE31} 49 | EndGlobalSection 50 | GlobalSection(ExtensibilityGlobals) = postSolution 51 | SolutionGuid = {F5CB5E11-069A-415B-87C7-36EF1F8937F9} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Leonard Sperry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Below is license information for dependant packages: 24 | 25 | NLog - BSD 3: 26 | Copyright (c) 2004-2021 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen 27 | 28 | All rights reserved. 29 | 30 | Redistribution and use in source and binary forms, with or without 31 | modification, are permitted provided that the following conditions 32 | are met: 33 | 34 | * Redistributions of source code must retain the above copyright notice, 35 | this list of conditions and the following disclaimer. 36 | 37 | * Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the documentation 39 | and/or other materials provided with the distribution. 40 | 41 | * Neither the name of Jaroslaw Kowalski nor the names of its 42 | contributors may be used to endorse or promote products derived from this 43 | software without specific prior written permission. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 46 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 47 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 48 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 49 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 50 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 51 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 52 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 53 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 54 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 55 | THE POSSIBILITY OF SUCH DAMAGE. 56 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/Automations/AutomationWithPreStartupTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.Extensions.Logging; 3 | using Moq; 4 | 5 | namespace HaKafkaNet.ExampleApp.Tests; 6 | 7 | public class SimpleAutomationTests 8 | { 9 | [Fact] 10 | public async Task WhenTestButtonPushedAfterStartup_SendsNotification() 11 | { 12 | //arrange 13 | Mock mockApi = new Mock(); 14 | Mock> logger = new(); 15 | 16 | AutomationWithPreStartup sut = new AutomationWithPreStartup(mockApi.Object, logger.Object); 17 | 18 | var stateChange = getFakeStateChange(); 19 | 20 | // act 21 | await sut.Execute(stateChange, default); 22 | 23 | // assert 24 | mockApi.Verify(a => a.PersistentNotification(It.IsAny(), default), Times.Once); 25 | } 26 | 27 | private HaEntityStateChange getFakeStateChange() 28 | { 29 | return new HaEntityStateChange() 30 | { 31 | EntityId = "input_button.test_button", 32 | EventTiming = EventTiming.PostStartup, 33 | New = getButtonPush() 34 | }; 35 | } 36 | 37 | private HaEntityState getButtonPush() 38 | { 39 | return new HaEntityState() 40 | { 41 | EntityId = "input_button.test_button", 42 | State = "I exist", 43 | Attributes = new JsonElement() 44 | }; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/HaKafkaNet.ExampleApp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/HaKafkanetFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using HaKafkaNet; 3 | using HaKafkaNet.Testing; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Mvc.Testing; 6 | using Microsoft.AspNetCore.TestHost; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Time.Testing; 9 | using Moq; 10 | 11 | /// 12 | /// Reminder: Updates to the framework may require updates to this file. 13 | /// If there are breaking changes to the framework re-copy this file from 14 | /// https://raw.githubusercontent.com/leosperry/ha-kafka-net/refs/heads/main/example/HaKafkaNet.ExampleApp.Tests/HaKafkanetFixture.cs 15 | /// 16 | public class HaKafkaNetFixture : WebApplicationFactory 17 | { 18 | public Mock API { get; } = new Mock(); 19 | public TestHelper Helpers { get => Services.GetRequiredService(); } 20 | 21 | public HaKafkaNetFixture() 22 | { 23 | // todo: find a better setup 24 | // calling helpers here will cause an infinite loop at startup when active automations are used 25 | // or anything needing IHaApiProvider or FakeTimeProvider at startup 26 | this.API.Setup(api => api.GetEntity(It.IsAny(), It.IsAny())) 27 | .ReturnsAsync(new Func( 28 | (id, ct) => ( 29 | new HttpResponseMessage(System.Net.HttpStatusCode.OK), 30 | new HaEntityState() 31 | { 32 | EntityId = id, 33 | State = "0", 34 | Attributes = JsonSerializer.SerializeToElement("{}"), 35 | LastChanged = DateTime.Now, 36 | LastUpdated = DateTime.Now 37 | }))); 38 | } 39 | 40 | protected override void ConfigureWebHost(IWebHostBuilder builder) 41 | { 42 | builder.UseEnvironment("Test"); // add an appsettings.Test.json file to your application 43 | 44 | builder.ConfigureServices(services => { 45 | // call this method with the fake or mock of your choice 46 | // optionally pass an IDistributed cache. 47 | services.ConfigureForIntegrationTests(API.Object); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/IntegrationTests/ActiveTests.cs: -------------------------------------------------------------------------------- 1 | // using System; 2 | // using HaKafkaNet.Testing; 3 | // using Moq; 4 | 5 | // namespace HaKafkaNet.ExampleApp.Tests.IntegrationTests; 6 | 7 | // public class ActiveTests : IClassFixture 8 | // { 9 | // private readonly HaKafkaNetFixture _fixture; 10 | // private readonly TestHelper _testHelper; 11 | 12 | // public ActiveTests(HaKafkaNetFixture fixture) 13 | // { 14 | // this._fixture = fixture; 15 | // this._testHelper = fixture.Helpers; 16 | // } 17 | 18 | // // this test seems to hang the test runner when run via git actions 19 | 20 | // [Fact] 21 | // public Task ActiveFiresOnStartup() 22 | // { 23 | // _fixture.API.Verify(api => api.ButtonPress("my.button", default)); 24 | // return Task.CompletedTask; 25 | // } 26 | // } 27 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/IntegrationTests/LightOnRegistryTests.cs: -------------------------------------------------------------------------------- 1 | using HaKafkaNet; 2 | using HaKafkaNet.Testing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Moq; 5 | using System.Text.Json; 6 | 7 | namespace HaKafkaNet.ExampleApp.Tests; 8 | 9 | public class LightOnRegistryTests : IClassFixture 10 | { 11 | private HaKafkaNetFixture _fixture; 12 | 13 | public LightOnRegistryTests(HaKafkaNetFixture fixture) 14 | { 15 | this._fixture = fixture; 16 | } 17 | 18 | [Fact] 19 | public async Task LightOnRegistry_TurnsOnLights() 20 | { 21 | // Given 22 | _fixture.API.Setup(api => api.GetEntity>(LightOnRegistry.OFFICE_LIGHT, It.IsAny())) 23 | .ReturnsAsync(_fixture.Helpers.Api_GetEntity_Response(OnOff.Off)); 24 | 25 | // When 26 | var motionOnState = new HaEntityState() 27 | { 28 | EntityId = LightOnRegistry.OFFICE_MOTION, 29 | State = OnOff.On, 30 | Attributes = new { }, 31 | LastChanged = DateTime.UtcNow.AddMinutes(1), 32 | LastUpdated = DateTime.UtcNow.AddMinutes(1), 33 | }; 34 | 35 | await _fixture.Helpers.SendState(motionOnState, 300); 36 | 37 | // Then 38 | _fixture.API.Verify(api => api.TurnOn(LightOnRegistry.OFFICE_LIGHT, It.IsAny()), Times.Exactly(5)); 39 | _fixture.API.Verify(api => api.LightSetBrightness(LightOnRegistry.OFFICE_LIGHT, 200, It.IsAny())); 40 | // six similar automations set up different ways 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/AdvancedTutorialRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet.ExampleApp; 2 | 3 | public class AdvancedTutorialRegistry : IAutomationRegistry 4 | { 5 | readonly IAutomationBuilder _builder; 6 | readonly IHaServices _services; 7 | 8 | public AdvancedTutorialRegistry(IAutomationBuilder builder, IHaServices services) 9 | { 10 | _builder = builder; 11 | _services = services; 12 | } 13 | public void Register(IRegistrar reg) 14 | { 15 | reg.TryRegister( 16 | () => DoorAlert("binary_sensor.front_door_contact", "front door"), 17 | () => DoorAlert("binary_sensor.back_door_contact", "back door") 18 | ); 19 | } 20 | 21 | IConditionalAutomation DoorAlert(string entityId, string friendlyName) 22 | { 23 | const int seconds = 10; 24 | 25 | return _builder.CreateConditional() 26 | .WithName($"{friendlyName} open alert") 27 | .WithDescription($"Notify when the {friendlyName} has been open for more than {seconds} seconds") 28 | .When((sc) => sc.ToOnOff().IsOn()) 29 | .ForSeconds(seconds) 30 | .Then(ct => NotifyDoorOpen(entityId, friendlyName, TimeSpan.FromSeconds(seconds), ct)) 31 | .Build(); 32 | } 33 | 34 | private async Task NotifyDoorOpen(string entityId, string friendlyName, TimeSpan seconds, CancellationToken ct) 35 | { 36 | // if we get here, the door has been open for 10 seconds 37 | string message = $"{friendlyName} is open"; 38 | bool doorOpen = true; 39 | int alertCount = 0; 40 | try 41 | { 42 | do 43 | { 44 | await _services.Api.Speak("tts.piper", "media_player.kitchen", message, cancellationToken: ct); 45 | 46 | await Task.Delay(seconds, ct); // <-- use the cancellation token 47 | 48 | var doorState = await _services.EntityProvider.GetOnOffEntity(entityId, ct); 49 | doorOpen = doorState!.IsOn(); 50 | } while (doorOpen && ++alertCount < 12 && !ct.IsCancellationRequested); 51 | 52 | if (doorOpen) 53 | { 54 | await _services.Api.NotifyGroupOrDevice("mobile_app_my_phone", message, cancellationToken: ct); 55 | } 56 | } 57 | catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException) 58 | { 59 | // don't do anything 60 | // the door was closed or 61 | // the application is shutting down 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/AutomationWithPreStartup.cs: -------------------------------------------------------------------------------- 1 | using HaKafkaNet; 2 | 3 | namespace HaKafkaNet.ExampleApp; 4 | 5 | /// 6 | /// This automation demonstrates 2-way communication with Home Assitant and handling events which happened prior to startup 7 | /// It assumes you have created a Helper Button in Home Assistant named Test Button. It should have an id of "input_button.test_button". 8 | /// When that button is pushed it sends a notification to Home assistant. 9 | /// If the button was pushed before startup, a message is written to the console, but no notiication is sent 10 | /// To see the 4 event timings in action 11 | /// * clear cache, click the button, then start this app 12 | /// * with the app running, click the button, watch the notification go through, then restart app 13 | /// * stop the app, click the button, then restart the app 14 | /// 15 | public class AutomationWithPreStartup : IAutomation 16 | { 17 | IHaApiProvider _api; 18 | ILogger _logger; 19 | 20 | public AutomationWithPreStartup(IHaApiProvider haApiProvider, ILogger logger) 21 | { 22 | _api = haApiProvider; 23 | _logger = logger; 24 | } 25 | 26 | public string Name { get => "Automation with Pre-startup events handled"; } 27 | 28 | public EventTiming EventTimings 29 | { 30 | get => EventTiming.PreStartupNotCached | EventTiming.PreStartupSameAsLastCached | EventTiming.PreStartupPostLastCached | EventTiming.PostStartup; 31 | } 32 | 33 | public IEnumerable TriggerEntityIds() 34 | { 35 | yield return "input_button.test_button"; 36 | } 37 | 38 | public Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 39 | { 40 | var message = $"test button last changed at : {stateChange.New.LastChanged}"; 41 | 42 | switch (stateChange.EventTiming) 43 | { 44 | case EventTiming.PreStartupNotCached: 45 | case EventTiming.PreStartupSameAsLastCached: 46 | case EventTiming.PreStartupPostLastCached: 47 | _logger.LogInformation(message + " - {timing}", stateChange.EventTiming); 48 | return Task.CompletedTask; 49 | case EventTiming.PostStartup: 50 | _logger.LogInformation("Sending Persistent Notification"); 51 | return _api.PersistentNotification(message, cancellationToken); 52 | default: 53 | return Task.CompletedTask; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/ConditionalAutomationExample.cs: -------------------------------------------------------------------------------- 1 | using Confluent.Kafka; 2 | 3 | namespace HaKafkaNet.ExampleApp; 4 | 5 | public class ConditionalAutomationExample : IConditionalAutomation, IAutomationMeta 6 | { 7 | 8 | private int _buttonTracker = 0; 9 | private bool _colorTracker = default; 10 | private readonly IHaServices _services; 11 | private readonly ILogger _logger; 12 | const string LIGHT_ID = "light.office_led_light"; 13 | 14 | public ConditionalAutomationExample(IHaServices services, ILogger logger) 15 | { 16 | this._services = services; 17 | this._logger = logger; 18 | } 19 | 20 | //interface implementations 21 | public IEnumerable TriggerEntityIds() 22 | { 23 | yield return "input_button.test_button_3"; 24 | } 25 | 26 | public Task ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken cancellationToken) 27 | { 28 | _buttonTracker++; 29 | _logger.LogInformation("tracker = {value}", _buttonTracker); 30 | // simulate that a motion sensor could report multiple times, a condition that should not cancel, and then eventually does 31 | // like a motion sensor reporting clear or "off". 32 | return Task.FromResult(!(_buttonTracker % 3 == 0)); 33 | } 34 | 35 | public TimeSpan For => TimeSpan.FromSeconds(5); 36 | 37 | public Task Execute(CancellationToken cancellationToken) 38 | { 39 | // time elapsed without canceling and we are now executing 40 | // reset the tracker back to known state 41 | _buttonTracker = 0; 42 | 43 | LightTurnOnModel color1 = new LightTurnOnModel() 44 | { 45 | EntityId = [LIGHT_ID], 46 | RgbColor = (255, 255, 0) 47 | }; 48 | 49 | LightTurnOnModel color2 = new LightTurnOnModel() 50 | { 51 | EntityId = [LIGHT_ID], 52 | RgbColor = (255, 0, 255) 53 | }; 54 | 55 | return _services.Api.LightTurnOn((_colorTracker = !_colorTracker) ? color1 : color2, cancellationToken); 56 | } 57 | 58 | public AutomationMetaData GetMetaData() 59 | { 60 | return new() 61 | { 62 | Name = "Example conditional automation", 63 | Description = "Sets some lights when a test button is pushed", 64 | }; 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/ExampleDurableAutomation.cs: -------------------------------------------------------------------------------- 1 | using HaKafkaNet; 2 | 3 | namespace MyHome.Dev; 4 | 5 | /// 6 | /// https://github.com/leosperry/ha-kafka-net/wiki/Durable-Automations 7 | /// 8 | [ExcludeFromDiscovery] //remove this line in your implementation 9 | public class ExampleDurableAutomation : ISchedulableAutomation 10 | { 11 | private DateTimeOffset? _nextScheduled; 12 | 13 | public bool IsReschedulable => true; 14 | 15 | /// 16 | /// This property is a part of IAutomation and has a default implementation 17 | /// To create a durable automation, you should override this behavior 18 | /// 19 | public EventTiming EventTimings { get => EventTiming.Durable; } 20 | 21 | /// 22 | /// This property is a part of IDelayableAutomation and has a default implementation 23 | /// If you want to handle events when the time elapsed prior to start up (during a restart) 24 | /// you should override this behavior and return true. 25 | /// 26 | public bool ShouldExecutePastEvents { get => true; } 27 | 28 | public ExampleDurableAutomation(/*inject any services you need*/) 29 | { 30 | 31 | } 32 | 33 | public Task ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken ct) 34 | { 35 | /* 36 | this method will be called when a state change happens 37 | you should track what time you want the automation to run 38 | in this case you would set _nextScheduled. 39 | If this method returns true, GetNextScheduled will be called 40 | */ 41 | bool shouldContinue = false; // add your logic here 42 | if (shouldContinue) 43 | { 44 | // set _nextScheduled 45 | _nextScheduled = haEntityStateChange.New.LastChanged.AddMinutes(5); 46 | } 47 | else 48 | { 49 | _nextScheduled = null; 50 | } 51 | return Task.FromResult(shouldContinue); 52 | } 53 | 54 | public Task Execute(CancellationToken ct) 55 | { 56 | // add your execution logic here 57 | return Task.CompletedTask; 58 | } 59 | 60 | public DateTimeOffset? GetNextScheduled() 61 | { 62 | /* 63 | the framework will call this method to get the next scheduled time 64 | if you return null here, it will cancel the automation the same 65 | as if ContinuesToBeTrue returned false 66 | */ 67 | return _nextScheduled; 68 | } 69 | 70 | public IEnumerable TriggerEntityIds() 71 | { 72 | yield return "domain.entity"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/ExampleDurableAutomation2.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HaKafkaNet.ExampleApp; 3 | 4 | [ExcludeFromDiscovery] //remove this line in your implementation 5 | public class ExampleDurableAutomation2 : SchedulableAutomationBase 6 | { 7 | // constants defined for code clarity only 8 | const bool _shouldExecutePast = true; 9 | const bool _shouldExecuteOnError = true; 10 | 11 | public ExampleDurableAutomation2(IEnumerable triggerIds) 12 | : base(triggerIds, _shouldExecutePast, _shouldExecuteOnError) 13 | { 14 | // set these values appropriately 15 | // https://github.com/leosperry/ha-kafka-net/wiki/Automation-Types#ischedulableautomation 16 | this.IsReschedulable = false; 17 | 18 | // https://github.com/leosperry/ha-kafka-net/wiki/Event-Timings#druable 19 | this.EventTimings = EventTiming.DurableIfCached; 20 | } 21 | 22 | /// 23 | /// This method replaces Continues to be true 24 | /// 25 | /// 26 | /// 27 | /// 28 | protected override Task CalculateNext(HaEntityStateChange stateChange, CancellationToken cancellationToken) 29 | { 30 | // returning null is the same as ContinuesToBeTrue returning false 31 | // if you want the automation to continue, you must return a non-null value 32 | // if your automation is not reschedulable, the value will be ignored 33 | 34 | // in this example we will take an action 1 hour after an entity turns on 35 | if (stateChange.ToOnOff().New.State == OnOff.On) 36 | { 37 | return Task.FromResult(stateChange.New.LastUpdated.AddHours(1)); 38 | } 39 | 40 | // the entity was off, cancel execution 41 | return Task.FromResult(default); 42 | } 43 | 44 | public override Task Execute(CancellationToken cancellationToken) 45 | { 46 | // you execution logic here 47 | return Task.CompletedTask; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/ExceptionTrowingAutomation.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HaKafkaNet.ExampleApp; 3 | 4 | /// 5 | /// This automation is for demonstration/testing purposes 6 | /// The execute method has several exceptions that can be 7 | /// commented/uncomented for testing different scenarios 8 | /// 9 | [ExcludeFromDiscovery] 10 | public class ExceptionTrowingAutomation : IConditionalAutomation, IAutomationMeta 11 | { 12 | readonly ILogger _logger; 13 | readonly TimeSpan _for = TimeSpan.FromSeconds(5); 14 | public TimeSpan For => _for; 15 | 16 | private bool _switchState = false; 17 | 18 | public ExceptionTrowingAutomation(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | public Task ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken _) 24 | { 25 | var onOff = haEntityStateChange.ToOnOff(); 26 | _switchState = onOff.IsOn(); 27 | if (_switchState) 28 | { 29 | _logger.LogWarning("The switch is on at: {OnTime}", haEntityStateChange.New.LastUpdated); 30 | } 31 | else 32 | { 33 | _logger.LogInformation("The switch is off at {OffTime}", haEntityStateChange.New.LastUpdated); 34 | } 35 | 36 | return Task.FromResult(true); 37 | } 38 | 39 | public Task Execute(CancellationToken ct) 40 | { 41 | if (_switchState) 42 | { 43 | _logger.LogWarning("Switch is still on!"); 44 | } 45 | else 46 | { 47 | // use this WhenAll to test an un-awaited exception. 48 | // The tracing system should report both errors 49 | // return Task.WhenAll( 50 | // Task.Delay(100).ContinueWith(t => throw new Exception("ha ha")), 51 | // Task.Delay(100).ContinueWith(t => throw new Exception("ho ho")) 52 | // ); 53 | try 54 | { 55 | throw new HaKafkaNetException("Example Exception!!!"); 56 | } 57 | catch (System.Exception ex) 58 | { 59 | _logger.LogError(ex, ex.Message); 60 | } 61 | 62 | throw new Exception("Test Override Exception"); 63 | } 64 | return Task.CompletedTask; 65 | } 66 | 67 | public IEnumerable TriggerEntityIds() 68 | { 69 | yield return "input_boolean.test_switch"; 70 | } 71 | 72 | readonly AutomationMetaData _meta = new() 73 | { 74 | Name = "Example Logging and Exceptions", 75 | Description = "This automation is for Trace and Log Capturing demonstration purposes. It logs at various levels and conditinally throws an exception.", 76 | Enabled = false 77 | }; 78 | public AutomationMetaData GetMetaData() => _meta; 79 | } 80 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/LightOnCustomAutomation.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HaKafkaNet.ExampleApp; 3 | 4 | /// 5 | /// Very important if you want to resuse this autommation in the registry for multiple devices that 6 | /// you decorate with the ExcludeFromDiscovery attribute 7 | /// 8 | [ExcludeFromDiscovery] 9 | public class LightOnCustomAutomation : IAutomation, IAutomationMeta 10 | { 11 | private readonly IHaApiProvider _api; 12 | private readonly string _motionId; 13 | private readonly string _lightId; 14 | private readonly byte _brightness; 15 | readonly AutomationMetaData _meta; 16 | 17 | public LightOnCustomAutomation(IHaApiProvider api, string motionId, string lightId, byte brightness, string name, string description) 18 | { 19 | _api = api; 20 | _motionId = motionId; 21 | _lightId = lightId; 22 | _brightness = brightness; 23 | _meta = new AutomationMetaData() 24 | { 25 | Name = name, 26 | Description = description, 27 | }; 28 | } 29 | 30 | public Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 31 | { 32 | var motion = stateChange.ToOnOff(); 33 | if ((motion.Old is null || motion.Old.State != OnOff.On) && motion.New.State == OnOff.On) 34 | { 35 | return _api.LightSetBrightness(_lightId, _brightness, cancellationToken); 36 | } 37 | return Task.CompletedTask; 38 | } 39 | 40 | public IEnumerable TriggerEntityIds() 41 | { 42 | yield return _motionId; 43 | } 44 | 45 | // IAutomationMeta implementation 46 | // you could omit this and use the extension method 47 | // as shown in LightOnRegistry.cs 48 | public AutomationMetaData GetMetaData() => _meta; 49 | } 50 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/MotionBehaviorTutorial.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using HaKafkaNet; 3 | namespace MyHome.Dev; 4 | 5 | /// 6 | /// https://github.com/leosperry/ha-kafka-net/wiki/Tutorial:-Creating-Automations 7 | /// 8 | [ExcludeFromDiscovery] //remove this line in your implementation 9 | public class MotionBehaviorTutorial : IAutomation, IAutomationMeta 10 | { 11 | readonly string _motion, _light; 12 | readonly IHaServices _services; 13 | public MotionBehaviorTutorial(string motion, string light, IHaServices services) 14 | { 15 | _motion = motion; 16 | _light = light; 17 | _services = services; 18 | } 19 | 20 | public IEnumerable TriggerEntityIds() => [_motion]; 21 | 22 | public async Task Execute(HaEntityStateChange> stateChange, CancellationToken ct) 23 | { 24 | if (stateChange.New.IsOff()) return; // don't do anything if the motion is not detected 25 | 26 | var homeState = await _services.EntityProvider.GetPersonEntity("person.name", ct); 27 | var isHome = homeState?.IsHome() ?? false; 28 | 29 | if (isHome) 30 | await _services.Api.TurnOn(_light); 31 | else 32 | await _services.Api.NotifyGroupOrDevice( 33 | "device_tracker.my_phone", $"Motion was detected by {_motion}", cancellationToken: ct); 34 | } 35 | 36 | public AutomationMetaData GetMetaData() => 37 | new() 38 | { 39 | Name = $"Motion Behavior{_motion}", 40 | Description = $"Turn on {_light} if we're home, otherwise notify", 41 | AdditionalEntitiesToTrack = [_light] 42 | }; 43 | } 44 | 45 | static class FactoryExtensions 46 | { 47 | public static IAutomation CreateMotionBehavior(this IAutomationFactory factory, string motion, string light) 48 | => new MotionBehaviorTutorial(motion, light, factory.Services); 49 | } -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/SceneControllerAutomation.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace HaKafkaNet.ExampleApp; 3 | 4 | /// 5 | /// https://github.com/leosperry/ha-kafka-net/wiki/Scene-Controllers 6 | /// 7 | [ExcludeFromDiscovery] //remove this line in your implementation 8 | public class SceneControllerAutomation : IAutomation_SceneController 9 | { 10 | public Task Execute(HaEntityStateChange> stateChange, CancellationToken ct) 11 | { 12 | if (!stateChange.New.StateAndLastUpdatedWithin1Second()) return Task.CompletedTask; 13 | 14 | var btn = stateChange.EntityId.Last(); 15 | var key = stateChange.New.Attributes?.GetKeyPress(); 16 | 17 | return (btn, key) switch 18 | { 19 | {btn: '1', key: KeyPress.KeyPressed} => HandleKey1Pressed(), 20 | {btn: '2', key: KeyPress.KeyPressed} => HandleKey2Pressed(), 21 | {btn: '3' or '4', key: KeyPress.KeyPressed2x} => HandleKey3or4DoublePressed(), 22 | _ => Task.CompletedTask 23 | }; 24 | } 25 | 26 | // implement and await as needed 27 | 28 | private Task HandleKey3or4DoublePressed() => Task.CompletedTask; 29 | 30 | private Task HandleKey2Pressed() => Task.CompletedTask; 31 | 32 | private Task HandleKey1Pressed() => Task.CompletedTask; 33 | 34 | public IEnumerable TriggerEntityIds() 35 | { 36 | yield return "event.my_scene_controller_001"; 37 | yield return "event.my_scene_controller_002"; 38 | yield return "event.my_scene_controller_003"; 39 | yield return "event.my_scene_controller_004"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/SimpleLightAutomation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | 5 | namespace HaKafkaNet.ExampleApp; 6 | 7 | /// 8 | /// Simple automation to demonstrate getting typed states from cache 9 | /// it assumes you have a helper button named "Test Button 2" 10 | /// change the id of the light for your setup 11 | /// 12 | public class SimpleLightAutomation : IAutomation, IAutomationMeta 13 | { 14 | IHaServices _services; 15 | string _idOfLightToDim; 16 | 17 | public SimpleLightAutomation(IHaServices services) 18 | { 19 | _services = services; 20 | _idOfLightToDim = "light.office_lights"; 21 | } 22 | 23 | public IEnumerable TriggerEntityIds() 24 | { 25 | yield return "input_button.test_button_2"; 26 | } 27 | 28 | public async Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 29 | { 30 | // the entity provider will attempt to get an entity from the cache and fall back to an api call 31 | var currentLightState = await _services.EntityProvider.GetColorLightEntity(_idOfLightToDim); 32 | if (currentLightState == null) 33 | { 34 | return; 35 | } 36 | var brightness = currentLightState.Attributes!.Brightness; 37 | 38 | //call a service to change it 39 | await _services.Api.CallService("light", "turn_on", new { 40 | entity_id = _idOfLightToDim, 41 | brightness = brightness - 5 42 | }, cancellationToken); 43 | } 44 | 45 | public AutomationMetaData GetMetaData() 46 | { 47 | return new() 48 | { 49 | Name = "Simple Automation", 50 | Description = "When button is pressed, dims a light" 51 | }; 52 | } 53 | 54 | record LightAttributes 55 | { 56 | [JsonPropertyName("brightness")] 57 | public byte Brightness { get; set; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/TemplateRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HaKafkaNet; 3 | 4 | namespace HaKafkaNet.ExampleApp.Automations; 5 | 6 | [ExcludeFromDiscovery]// IMPORTANT : REMOVE THIS LINE FROM YOUR IMPLEMENTATION 7 | public class TemplateRegistry : IAutomationRegistry, IInitializeOnStartup 8 | { 9 | readonly IStartupHelpers _helpers; 10 | readonly IHaServices _services; 11 | readonly ILogger _logger; 12 | 13 | public TemplateRegistry(IStartupHelpers startupHelpers, IHaServices service, ILogger logger) 14 | { 15 | this._helpers = startupHelpers; 16 | this._services = service; 17 | this._logger = logger; 18 | } 19 | 20 | public Task Initialize() 21 | { 22 | return Task.CompletedTask; 23 | } 24 | 25 | public void Register(IRegistrar reg) 26 | { 27 | reg.Register( 28 | Simple1(), 29 | Simple2() 30 | ); 31 | 32 | reg.RegisterDelayed( 33 | Delay1() 34 | ); 35 | } 36 | 37 | IAutomation Simple1() 38 | { 39 | return _helpers.Builder.CreateSimple() 40 | // fill in automation 41 | .WithExecution(async (sc, ct) => await Task.CompletedTask) 42 | .Build(); 43 | } 44 | 45 | IAutomation Simple2() 46 | { 47 | return _helpers.Factory.LightOnMotion("binary_sensor.motion_id", "light.light_id"); 48 | } 49 | 50 | IDelayableAutomation Delay1() 51 | { 52 | return _helpers.Builder.CreateConditional() 53 | .When(sc => false) 54 | .WithExecution(async ct => await Task.CompletedTask) 55 | .Build(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Automations/UpdatingEntityRegistry.cs: -------------------------------------------------------------------------------- 1 | using HaKafkaNet; 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet.ExampleApp.Automations; 5 | 6 | public class UpdatingEntityRegistry : IAutomationRegistry 7 | { 8 | readonly IAutomationBuilder _builder; 9 | readonly IHaApiProvider _api; 10 | readonly IHaEntity _illuminationSensor; 11 | 12 | public UpdatingEntityRegistry(IUpdatingEntityProvider updatingEntityProvider, 13 | IAutomationBuilder builder, IHaApiProvider api) 14 | { 15 | this._builder = builder; 16 | this._api = api; 17 | 18 | this._illuminationSensor = updatingEntityProvider.GetFloatEntity("sensor.illumination_sensor"); 19 | } 20 | 21 | public void Register(IRegistrar reg) 22 | { 23 | reg.Register(_builder.CreateSimple() 24 | .WithName("Turn On Light") 25 | .WithTriggers("binary_sensor.motion_sensor") 26 | .WithExecution(async (sc, ct) => { 27 | if (sc.ToOnOff().New.IsOn() && _illuminationSensor.State < 100) 28 | { 29 | await _api.TurnOn("light.my_light"); 30 | } 31 | }) 32 | .Build()); 33 | } 34 | } -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env 2 | WORKDIR /app 3 | 4 | COPY *.sln ./ 5 | COPY HaKafkaNet.ExampleApp/HaKafkaNet.ExampleApp.csproj ./HaKafkaNet.ExampleApp/HaKafkaNet.ExampleApp.csproj 6 | COPY HaKafkaNet.ExampleApp/appsettings.Production.json ./HaKafkaNet.ExampleApp/appsettings.json 7 | # Use the next line if setting up HaKafkaNet as a sub-module to your repo 8 | # COPY ha-kafka-net/src/HaKafkaNet/*.csproj ./ha-kafka-net/src/HaKafkaNet/ 9 | 10 | RUN dotnet restore HaKafkaNet.ExampleApp/HaKafkaNet.ExampleApp.csproj 11 | 12 | # Copy everything else and build 13 | COPY . ./ 14 | RUN dotnet publish -c Release -o out ./HaKafkaNet.ExampleApp/HaKafkaNet.ExampleApp.csproj 15 | 16 | # Build runtime image 17 | FROM mcr.microsoft.com/dotnet/aspnet:9.0 18 | # Set your timezone 19 | ENV TZ="US/Eastern" 20 | WORKDIR /app 21 | COPY --from=build-env /app/out . 22 | ENTRYPOINT ["dotnet", "MyHome.dll"] -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/HaKafkaNet.ExampleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | net9.0 23 | enable 24 | enable 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Models/AutoGen.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MyHome.Dev; 3 | 4 | namespace HaKafkaNet.ExampleApp.Models; 5 | 6 | /* 7 | This file represents an example output when using the template found here: 8 | https://github.com/leosperry/ha-kafka-net/blob/main/infrastructure/hakafkanet.jinja 9 | 10 | It is used in the example app integration tests 11 | */ 12 | 13 | public class Binary_Sensor 14 | { 15 | public const string MotionForSimple = "binary_sensor.motion_for_simple"; 16 | public const string MotionForSimpleTyped = "binary_sensor.motion_for_simple_typed"; 17 | public const string MotionForConditional = "binary_sensor.motion_for_conditional"; 18 | public const string MotionForConditionalTyped = "binary_sensor.motion_for_conditional_typed"; 19 | public const string MotionForSchedulable = "binary_sensor.motion_for_schedulable"; 20 | public const string MotionForSchedulableTyped = "binary_sensor.motion_for_schedulable_typed"; 21 | public const string TriggerForLongDelay = "binary_sensor.trigger_for_long_delay"; 22 | 23 | 24 | } 25 | 26 | public class Input_Button 27 | { 28 | public const string HelperButtonForSimple = "input_button.helper_button_for_simple"; 29 | public const string HelperButtonForSimpleTyped = "input_button.helper_button_for_simple_typed"; 30 | public const string HelperButtonForConditional = "input_button.helper_button_for_conditional"; 31 | public const string HelperButtonForConditionalTyped = "input_button.helper_button_for_conditional_typed"; 32 | public const string HelperButtonForSchedulable = "input_button.helper_button_for_schedulable"; 33 | public const string HelperButtonForSchedulableTyped = "input_button.helper_button_for_schedulable_typed"; 34 | public const string HelperButtonForLongDelay = "input_button.helper_button_for_long_delay"; 35 | 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:6178", 8 | "sslPort": 44376 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5062", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7069;http://localhost:5062", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/TestClasses/ActiveRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet.ExampleApp.TestClasses; 5 | 6 | public class ActiveRegistry : IAutomationRegistry 7 | { 8 | private readonly IAutomationBuilder _builder; 9 | private readonly IHaApiProvider _api; 10 | 11 | public ActiveRegistry(IAutomationBuilder builder, IHaApiProvider services) 12 | { 13 | this._builder = builder; 14 | this._api = services; 15 | } 16 | 17 | public void Register(IRegistrar reg) 18 | { 19 | reg.TryRegister(SimpleActive); 20 | } 21 | 22 | IAutomationBase SimpleActive() 23 | { 24 | return _builder.CreateSimple() 25 | .MakeActive() 26 | .WithTriggers("my.button") 27 | .WithExecution((sc, ct) => 28 | { 29 | _api.ButtonPress("my.button", default); 30 | return Task.CompletedTask; 31 | }) 32 | .Build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/TestClasses/readme.md: -------------------------------------------------------------------------------- 1 | Items in this folder are used as a part of the build for HaKafkaNet itself. -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "HaKafkaNet" : "Warning" 7 | } 8 | }, 9 | "NLog":{ 10 | "rules":[ 11 | { 12 | "logger": "Microsoft.*", 13 | "minLevel": "Warn", 14 | "finalMinLevel":"Warn" 15 | }, 16 | { 17 | "logger": "System.Net.*", 18 | "minLevel": "Warn", 19 | "finalMinLevel":"Info" 20 | }, 21 | { 22 | "logger": "HaKafkaNet.*", 23 | "minLevel": "Debug", 24 | "finalMinLevel": "Debug" 25 | } 26 | ] 27 | }, 28 | "AllowedHosts": "*", 29 | "HaKafkaNet": { 30 | "KafkaBrokerAddresses": [ ":9094" ], 31 | "KafkaTopic": "home_assistant_states", 32 | "ExposeKafkaFlowDashboard": true, 33 | "UseDashboard": true, 34 | "StateHandler": { 35 | "GroupId": "hakafkanet-consumer-example", 36 | "BufferSize": 5, 37 | "WorkerCount": 5 38 | }, 39 | "HaConnectionInfo": { 40 | "BaseUri": "http://IP_OR_DOMAIN_OF_YOUR_HA_INSTANCE:8123", 41 | "AccessToken": "YOUR_LONG_LIVED_HA_ACCESS_TOKEN" 42 | } 43 | }, 44 | "ConnectionStrings": { 45 | "RedisConStr" : "`YOUR_REDIS_CONNECTION_STRING" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | hakafkanet-example-app: 4 | build: 5 | dockerfile: HaKafkaNet.ExampleApp/Dockerfile 6 | context: . 7 | container_name: hakafkanet-example-app 8 | restart: unless-stopped 9 | environment: 10 | - ASPNETCORE_ENVIRONMENT=Production 11 | - DOTNET_ENVIRONMENT=Production 12 | ports: 13 | - 8082:8080 14 | 15 | -------------------------------------------------------------------------------- /images/HKN Social media banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/HKN Social media banner.png -------------------------------------------------------------------------------- /images/HaKafkaNetDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/HaKafkaNetDashboard.png -------------------------------------------------------------------------------- /images/HaKafkaNetDashboardV4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/HaKafkaNetDashboardV4.png -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-V5_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/AutomationDetail-V5_1.PNG -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-V5_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/AutomationDetail-V5_2.PNG -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-expanded-V5_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/AutomationDetail-expanded-V5_3.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-Detail-expanded-V5_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/Dashboard-Detail-expanded-V5_2.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/Dashboard-V5_1.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/Dashboard-V5_2.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/Dashboard-V5_5.PNG -------------------------------------------------------------------------------- /images/UI Examples/LogDetails.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/LogDetails.PNG -------------------------------------------------------------------------------- /images/UI Examples/Menu-V5_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/UI Examples/Menu-V5_5.PNG -------------------------------------------------------------------------------- /images/hkn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/hkn.png -------------------------------------------------------------------------------- /images/hkn_064.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/hkn_064.png -------------------------------------------------------------------------------- /images/hkn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/hkn_128.png -------------------------------------------------------------------------------- /images/hkn_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/hkn_256.png -------------------------------------------------------------------------------- /images/hkn_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/images/hkn_512.png -------------------------------------------------------------------------------- /infrastructure/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | kafka: 4 | image: bitnami/kafka:latest 5 | container_name: kafka 6 | restart: "unless-stopped" 7 | ports: 8 | - "2181:2181" 9 | - "9092:9092" 10 | - '9094:9094' 11 | environment: 12 | - KAFKA_CFG_NODE_ID=0 13 | - KAFKA_CFG_PROCESS_ROLES=controller,broker 14 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://0.0.0.0:9094 15 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://192.168.1.3:9094 # <----- modify EXTERNAL ip address. This makes it so that your HaKafkaNet instance can communicate with kafka 16 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT 17 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 18 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 19 | # This is completely optional and only needed if you want to persist data 20 | # HaKafkaNet is designed to be ephemeral see: https://github.com/leosperry/ha-kafka-net/wiki/Data-Persistence for details 21 | # the binami image uses a non-root account 22 | # this directory needs appropriate permissions set 23 | # in linux, this should be for uid 1001 24 | #volumes: 25 | # - /Path/to/persisted/data:/bitnami/kafka 26 | # - /home/leonard/MyData/kafka:/bitnami/kafka 27 | 28 | # initializes topics 29 | init-kafka: 30 | image: bitnami/kafka:latest 31 | depends_on: 32 | kafka: 33 | condition: service_started 34 | entrypoint: [ '/bin/sh', '-c' ] 35 | command: 36 | - | 37 | /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --list 38 | /opt/bitnami/kafka/bin/kafka-topics.sh --topic home_assistant_states --bootstrap-server kafka:9092 --if-not-exists --config "cleanup.policy=compact" --create 39 | /opt/bitnami/kafka/bin/kafka-configs.sh --bootstrap-server kafka:9092 --entity-type topics --entity-name home_assistant_states --alter --add-config max.compaction.lag.ms=1800000 40 | 41 | # recommended but optional 42 | # you must provide an IDistributed cache implementation 43 | # see https://github.com/leosperry/ha-kafka-net/wiki/Data-Persistence for additional information 44 | cache: 45 | image: redis:alpine 46 | container_name: redis 47 | restart: unless-stopped 48 | command: redis-server 49 | ports: 50 | - 6379:6379 51 | 52 | 53 | # kafka ui is optional. It is provided here for your convenience 54 | # can be used to inspect and customize your kafka instance 55 | kafka-ui: 56 | image: kafbat/kafka-ui 57 | container_name: kafka-ui 58 | depends_on: 59 | - kafka 60 | restart: "unless-stopped" 61 | ports: 62 | - "8080:8080" 63 | environment: 64 | KAFKA_CLUSTERS_0_NAME: local 65 | KAFKA_CLUSTERS_0_BOOTSTRAP_SERVERS: kafka:9092 66 | -------------------------------------------------------------------------------- /infrastructure/hakafkanet.jinja: -------------------------------------------------------------------------------- 1 | {# 2 | paste this code into your template editor 3 | http://homeassistant.local:8123/developer-tools/template 4 | 5 | It will render C# classes for you to quickly reference all your Entities, Labels, and Areas 6 | #} 7 | {% macro clean(str, parent) %}{% set name = str.title().replace('_', '') %}{% set cleanedName = ("_" + name) if name | regex_match('^\d+') or str.title() == parent else name %}{{ cleanedName }}{% endmacro %} 8 | 9 | {% for dd in states.input_select %} {% set enumName = clean(dd.entity_id.split('.')[1]) %} 10 | public enum {{ enumName }} 11 | { 12 | Unknown, Unavailable{% for val in state_attr(dd.entity_id, "options") %}, 13 | {{ clean(val) | regex_replace('[^\w]', '_') }}{% endfor %} 14 | } 15 | {% endfor %} 16 | public class Labels 17 | { {% for l in labels() %} 18 | public const string {{ clean(l, "Labels") }} = "{{ l }}";{% endfor %} 19 | } 20 | 21 | public class Areas 22 | { {% for a in areas() %} {% set name = a.title().replace('_', '') %}{% set cleanedName = ("_" + name) if name | regex_match('^\d+') or a.title() == "Labels" else name %} 23 | public const string {{ clean(a, "Areas") }} = "{{ a }}";{% endfor %} 24 | } 25 | {% for d in states | groupby('domain') %} 26 | public class {{ d[0].title() }} 27 | { {% for e in states[d[0]] %} 28 | public const string {{ clean(e.entity_id.split('.')[1] , d[0].title()) }} = "{{e.entity_id}}";{% endfor %} 29 | } 30 | {% endfor %} -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ha-kafka-net", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Moq; 2 | global using Xunit; -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/HaKafkaNet.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/Implementations/AutomationManagerTests/GetAllTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace HaKafkaNet.Tests; 4 | 5 | public class GetAllTests 6 | { 7 | [Fact] 8 | public void WhenNonePassedIn_returnsEmptyEnumerable() 9 | { 10 | // Given 11 | var autos = Enumerable.Empty(); 12 | 13 | Mock> logger = new(); 14 | Mock trace = new(); 15 | Mock observer = new(); 16 | Mock wrapperFactory = new(); 17 | 18 | var sut = new AutomationRegistrar(wrapperFactory.Object, 19 | autos, trace.Object, observer.Object, new List(), TimeProvider.System, logger.Object); 20 | // When 21 | 22 | var result = sut.RegisteredAutomations; 23 | 24 | // Then 25 | Assert.Empty(result); 26 | } 27 | 28 | [Fact] 29 | public void When1EachPassedIn_ReturnsAll() 30 | { 31 | // Given 32 | Mock auto = new(); 33 | IEnumerable autos = [auto.Object]; 34 | 35 | Mock conditional = new(); 36 | 37 | Mock schedulable = new(); 38 | 39 | Mock> logger = new(); 40 | Mock trace = new(); 41 | 42 | Mock observer = new(); 43 | 44 | Mock wrapperFactory = new(); 45 | wrapperFactory.Setup(w => w.GetWrapped(It.IsAny())) 46 | .Returns([new Mock().Object]); 47 | 48 | 49 | var sut = new AutomationRegistrar( 50 | wrapperFactory.Object, 51 | autos, trace.Object, observer.Object, new List(), TimeProvider.System, logger.Object); 52 | 53 | // When 54 | sut.Register(auto.Object); 55 | sut.RegisterDelayed(conditional.Object); 56 | sut.RegisterDelayed(schedulable.Object); 57 | var result = sut.RegisteredAutomations; 58 | 59 | // Then 60 | Assert.Equal(4, result.Count()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/Implementations/AutomationManagerTests/GetByKeyTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace HaKafkaNet.Tests; 4 | 5 | public class GetByKeyTests 6 | { 7 | [Fact] 8 | public void WheMetaNotSet_ReturnsByCleanedKey() 9 | { 10 | // Given 11 | Mock trace = new(); 12 | 13 | AutomationWrapper wrapper = new AutomationWrapper(new FakeAuto(), trace.Object, TimeProvider.System, "test"); 14 | 15 | Mock registry = new(); 16 | 17 | Mock registrar = new(); 18 | registrar.Setup(r => r.Registered) 19 | .Returns([wrapper]); 20 | 21 | IEnumerable registries = [registry.Object]; 22 | 23 | // When 24 | var sut = new AutomationManager(registries, registrar.Object); 25 | sut.Initialize(new List()); 26 | 27 | var result = sut.GetByKey("test-fakeauto-crew_spock-crew_evil_spock"); 28 | 29 | // Then 30 | Assert.Equal("FakeAuto", result!.GetMetaData().Name); 31 | } 32 | 33 | [Fact] 34 | public void WheMultiple_ReturnsByCleanedKey() 35 | { 36 | // Given 37 | Mock trace = new(); 38 | 39 | AutomationWrapper wrapper1 = new AutomationWrapper(new FakeAuto(), trace.Object, TimeProvider.System, "test"); 40 | AutomationWrapper wrapper2 = new AutomationWrapper(new FakeAuto(), trace.Object, TimeProvider.System, "test"); 41 | 42 | Mock registry = new(); 43 | 44 | Mock registrar = new(); 45 | registrar.Setup(r => r.Registered) 46 | .Returns([wrapper1, wrapper2]); 47 | 48 | IEnumerable registries = [registry.Object]; 49 | 50 | // When 51 | var sut = new AutomationManager(registries, registrar.Object); 52 | sut.Initialize(new List()); 53 | 54 | var result1 = sut.GetByKey("test-fakeauto-crew_spock-crew_evil_spock"); 55 | var result2 = sut.GetByKey("test-fakeauto-crew_spock-crew_evil_spock2"); 56 | 57 | // Then 58 | Assert.Equal("FakeAuto", result1!.GetMetaData().Name); 59 | Assert.Equal("FakeAuto", result2!.GetMetaData().Name); 60 | } 61 | 62 | [Fact] 63 | public void WheMetaSet_ReturnsByCleanedKey() 64 | { 65 | // Given 66 | var fake = new FakeAutoWithMeta(); 67 | fake.SetKey("!@#$ Evil !@#$ Spock !@#$"); 68 | Mock trace = new(); 69 | AutomationWrapper wrapper = new AutomationWrapper(fake, trace.Object, TimeProvider.System, "test"); 70 | 71 | Mock registry = new(); 72 | 73 | Mock registrar = new(); 74 | registrar.Setup(r => r.Registered) 75 | .Returns([wrapper]); 76 | 77 | IEnumerable registries = [registry.Object]; 78 | 79 | // When 80 | var sut = new AutomationManager(registries, registrar.Object); 81 | sut.Initialize(new List()); 82 | 83 | var result = sut.GetByKey("evil_spock"); 84 | 85 | // Then 86 | Assert.Equal("Spock", result!.GetMetaData().Name); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/Implementations/Automations/AutomationWrapperTests.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using FastEndpoints; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace HaKafkaNet.Tests; 7 | 8 | public class AutomationWrapperTests 9 | { 10 | [Fact] 11 | public void WhenMetaNotSet_CreatesMeta() 12 | { 13 | // Given 14 | FakeAuto auto = new(); 15 | Mock trace = new(); 16 | 17 | // When 18 | AutomationWrapper sut = new(auto, trace.Object, TimeProvider.System, "test"); 19 | 20 | // Then 21 | var meta = sut.GetMetaData(); 22 | Assert.Equal("FakeAuto", meta.Name); 23 | //$"{source}-{name}-{triggers}" 24 | Assert.Equal("test-FakeAuto-crew.spock-crew.evil_spock", meta.KeyRequest); 25 | } 26 | 27 | [Fact] 28 | public void WhenMetaSet_KeyRequestIsNot_SetsRequestWithName() 29 | { 30 | // Given 31 | FakeAutoWithMeta auto = new(); 32 | Mock trace = new(); 33 | 34 | // When 35 | AutomationWrapper sut = new(auto, trace.Object, TimeProvider.System, "test"); 36 | 37 | // Then 38 | var meta = sut.GetMetaData(); 39 | Assert.Equal("Spock", meta.Name); 40 | //$"{source}-{name}-{triggers}" 41 | Assert.Equal("Spock", meta.KeyRequest); 42 | } 43 | 44 | [Fact] 45 | public void WhenMetaSet_KeyRequestIs_SetsRequest() 46 | { 47 | // Given 48 | FakeAutoWithMeta auto = new(); 49 | auto.SetKey(); 50 | Mock trace = new(); 51 | 52 | // When 53 | AutomationWrapper sut = new(auto, trace.Object, TimeProvider.System, "test"); 54 | 55 | // Then 56 | var meta = sut.GetMetaData(); 57 | Assert.Equal("Spock", meta.Name); 58 | //$"{source}-{name}-{triggers}" 59 | Assert.Equal("Evil Spock", meta.KeyRequest); 60 | } 61 | } 62 | 63 | class FakeAuto : IAutomation 64 | { 65 | public Task Execute(HaEntityStateChange stateChange, CancellationToken ct) 66 | { 67 | return Task.CompletedTask; 68 | } 69 | 70 | public virtual IEnumerable TriggerEntityIds() 71 | { 72 | yield return "crew.spock"; 73 | yield return "crew.evil_spock"; 74 | } 75 | } 76 | 77 | class FakeAutoWithMeta : FakeAuto, IAutomationMeta 78 | { 79 | AutomationMetaData _meta = new() 80 | { 81 | Name = "Spock" 82 | }; 83 | 84 | public AutomationMetaData GetMetaData() 85 | { 86 | return _meta; 87 | } 88 | 89 | public override IEnumerable TriggerEntityIds() 90 | { 91 | return base.TriggerEntityIds().Union(["crew.evil_spock"]); 92 | } 93 | 94 | public void SetKey(string? key = default) 95 | { 96 | _meta.KeyRequest = key ?? "Evil Spock"; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/Implementations/HaEntityProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Castle.Core.Logging; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace HaKafkaNet.Tests; 6 | 7 | public class HaEntityProviderTests 8 | { 9 | [Fact] 10 | public async Task WhenInCache_ReturnsFromCache() 11 | { 12 | // Given 13 | Mock cache = new(); 14 | var fakeState = TestHelpers.GetState(); 15 | cache.Setup(c => c.GetEntity(fakeState.EntityId, default)) 16 | .ReturnsAsync(fakeState); 17 | 18 | Mock api = new(); 19 | 20 | Mock> logger = new(); 21 | 22 | HaEntityProvider sut = new HaEntityProvider(cache.Object, api.Object, logger.Object); 23 | 24 | // When 25 | var result = await sut.GetEntity(fakeState.EntityId); 26 | 27 | // Then 28 | cache.Verify(c => c.GetEntity(fakeState.EntityId, default)); 29 | api.Verify(a => a.GetEntity(It.IsAny(), default), Times.Never); 30 | Assert.Equal(fakeState, result); 31 | } 32 | 33 | [Fact] 34 | public async Task WhenNotInCache_GetsFromAPI() 35 | { 36 | // Given 37 | Mock cache = new(); 38 | HaEntityState fakeState = TestHelpers.GetState(); 39 | cache.Setup(c => c.GetEntity(fakeState.EntityId, default)) 40 | .ReturnsAsync(default(HaEntityState)).Verifiable(); 41 | 42 | Mock api = new(); 43 | (HttpResponseMessage, HaEntityState) apiReturn = (default(HttpResponseMessage), fakeState)!; 44 | api.Setup(a => a.GetEntity(fakeState.EntityId, default)) 45 | .ReturnsAsync(apiReturn).Verifiable(); 46 | 47 | Mock> logger = new(); 48 | 49 | HaEntityProvider sut = new HaEntityProvider(cache.Object, api.Object, logger.Object); 50 | 51 | // When 52 | var result = await sut.GetEntity(fakeState.EntityId); 53 | 54 | // Then 55 | cache.Verify(); 56 | api.Verify(); 57 | } 58 | 59 | [Fact] 60 | public async Task WhenCacheThrows_ReturnsFromApi() 61 | { 62 | // Given 63 | Mock cache = new(); 64 | HaEntityState fakeState = TestHelpers.GetState(); 65 | cache.Setup(c => c.GetEntity(fakeState.EntityId, default)) 66 | .Throws(new Exception()).Verifiable(); 67 | 68 | Mock api = new(); 69 | (HttpResponseMessage, HaEntityState) apiReturn = (default(HttpResponseMessage), fakeState)!; 70 | api.Setup(a => a.GetEntity(fakeState.EntityId, default)) 71 | .ReturnsAsync(apiReturn).Verifiable(); 72 | 73 | Mock> logger = new(); 74 | 75 | HaEntityProvider sut = new HaEntityProvider(cache.Object, api.Object, logger.Object); 76 | 77 | // When 78 | var result = await sut.GetEntity(fakeState.EntityId); 79 | 80 | // Then 81 | cache.Verify(); 82 | api.Verify(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/Implementations/Models/HaEntityStateConversionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Metadata; 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet.Tests; 5 | 6 | public class HaEntityStateConversionTests 7 | { 8 | [Fact] 9 | public void WhenStateIsDate_ConvertsCorrectly() 10 | { 11 | // Given 12 | SceneControllerEvent evt = new() 13 | { 14 | EventType = "self desctruct", 15 | }; 16 | var atts = JsonSerializer.SerializeToElement(evt); 17 | var state = new HaEntityState() 18 | { 19 | EntityId = "NCC-1701", 20 | State = DateTime.Now.ToString("o"), 21 | Attributes = atts, 22 | LastUpdated = DateTime.Now 23 | }; 24 | 25 | // When 26 | var typed = (HaEntityState)state; 27 | 28 | 29 | // Then 30 | Assert.NotNull(typed.State); 31 | Assert.NotNull(typed.Attributes?.EventType); 32 | } 33 | 34 | [Fact] 35 | public void WhenStateIsDouble_ConvertsCorrectly() 36 | { 37 | // Given 38 | var atts = JsonSerializer.SerializeToElement(new{}); 39 | var state = new HaEntityState() 40 | { 41 | EntityId = "NCC-1701", 42 | State = "1000.12345", 43 | Attributes = atts, 44 | LastUpdated = DateTime.Now 45 | }; 46 | 47 | // When 48 | var typed = (HaEntityState)state; 49 | 50 | 51 | // Then 52 | Assert.NotNull(typed.State); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/src/HaKafkaNet.UI/.env -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_API_URL=http://localhost:8082 -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_URl='' -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HaKafkaNet 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hakafkanet-ui", 3 | "private": true, 4 | "version": "10.2.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "^5.3.3", 14 | "bootstrap-icons": "^1.11.3", 15 | "react": "^18.3.1", 16 | "react-bootstrap": "^2.10.5", 17 | "react-dom": "^18.3.1" 18 | }, 19 | "devDependencies": { 20 | "@babel/types": "^7.26.3", 21 | "@types/react": "^18.3.1", 22 | "@types/react-dom": "^18.3.1", 23 | "@typescript-eslint/eslint-plugin": "^7.0.2", 24 | "@typescript-eslint/parser": "^7.0.2", 25 | "@vitejs/plugin-react": "^4.2.1", 26 | "eslint": "^8.56.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.5", 29 | "react-router-dom": "^6.23.1", 30 | "typescript": "^5.2.2", 31 | "vite": "^5.1.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/src/HaKafkaNet.UI/public/favicon.ico -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/App.css: -------------------------------------------------------------------------------- 1 | 2 | div.row{ 3 | width: 100%; 4 | } 5 | 6 | .automation-list-header { 7 | padding-left: 1.25rem; 8 | padding-right: 1.25rem; 9 | padding-bottom: .5rem; 10 | } -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | 3 | import HknHeaderFooter from "./components/HknHeaderFooter"; 4 | import AutomationList from "./components/AutomationList"; 5 | import AutomationDetails from "./components/AutomationDetails"; 6 | import ErrorLogs from "./components/ErrorLogs"; 7 | 8 | function App() { 9 | return (<> 10 | 11 | 12 | 13 | } /> 14 | } /> 15 | } /> 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default App; -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/assets/hkn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/686c1a4daf2a106e88dd3cb034d99f16be0e91c1/src/HaKafkaNet.UI/src/assets/hkn_128.png -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/components/AutomationList.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, InputGroup, Form } from "react-bootstrap"; 2 | import { AutomationData } from "../models/AutomationData"; 3 | import AutomationListItem from "./AutomationListItem"; 4 | import { useEffect, useState } from "react"; 5 | import icons from '../assets/icons/bootstrap-icons.svg'; 6 | import { Api } from "../services/Api"; 7 | 8 | function AutomationList() { 9 | 10 | const [data, setData] = useState(); 11 | 12 | useEffect(() => { 13 | getData(); 14 | }, []); 15 | 16 | async function getData() { 17 | var sysInfo = await Api.GetAutomationList(); 18 | setData(sysInfo.automations); 19 | } 20 | 21 | const [searchTxt, setSearchText] = useState(''); 22 | 23 | function filter(autodata: AutomationData[]): AutomationData[] { 24 | 25 | return autodata.filter(a => { 26 | const lowered = searchTxt.toLowerCase(); 27 | return searchTxt == '' || 28 | a.name.toLowerCase().includes(lowered) || 29 | a.description.toLowerCase().includes(lowered) || 30 | a.source.toLocaleLowerCase().includes(lowered) || 31 | a.triggerIds.filter(t => t.toLowerCase().includes(lowered)).length > 0 || 32 | a.additionalEntitiesToTrack.filter(t => t.toLowerCase().includes(lowered)).length > 0 33 | }); 34 | } 35 | 36 | return !data ? (<>Loading ...) : (<> 37 |
38 |
39 |

Automations

40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | setSearchText(e.target.value)} /> 50 | 51 |
52 |
53 | 54 | {data.length == 0 ? (

No Automation Found

) : (<> 55 |
56 |
Enabled
57 |
Name
58 |
Description
59 |
Source/Type
60 |
61 | 62 | 63 | {filter(data).map((item, index) => ())} 64 | 65 | )} 66 | ); 67 | } 68 | 69 | export default AutomationList; -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/components/AutomationListItem.tsx: -------------------------------------------------------------------------------- 1 | import { AutomationData } from "../models/AutomationData"; 2 | import { useEffect, useState } from "react"; 3 | import { Api } from "../services/Api"; 4 | import * as reactRouterDom from "react-router-dom"; 5 | import * as reactBootstrap from "react-bootstrap"; 6 | 7 | interface Props { 8 | item: AutomationData; 9 | index: number; 10 | } 11 | 12 | function AutomationListItem(props: Props) { 13 | const navigate = reactRouterDom.useNavigate(); 14 | 15 | const [enabled, setEnabled] = useState(props.item.enabled); 16 | 17 | useEffect(() => { 18 | setEnabled(props.item.enabled); 19 | }, []); 20 | 21 | function renderStringArray (arry : string[]) : string{ 22 | if (arry.length > 0) { 23 | return arry.reduce( (accumulator, currentValue) => accumulator + ", " + currentValue) 24 | } 25 | return ""; 26 | } 27 | 28 | async function handleCheckboxChange(e: React.ChangeEvent) { 29 | var autokey = e.target.getAttribute('data-key')!; 30 | var checked = e.target.checked; 31 | 32 | var response = await Api.EnableAutomation(autokey, checked); 33 | 34 | if ((response).ok) { 35 | setEnabled(!enabled) 36 | } 37 | } 38 | 39 | const item = props.item; 40 | 41 | return (<> 42 | 43 | 44 |
45 |
46 | e.stopPropagation()} onChange={handleCheckboxChange} checked={enabled} data-key={item.key} /> 47 |
48 |
{item.name}
49 |
{item.description}
50 |
51 |
{item.source}
52 |
{item.typeName}
53 |
54 |
55 |
56 | 57 |
58 |
59 | { navigate('/automation/' + item.key); e.preventDefault() }} >Details 60 |
Last Triggered: {item.lastTriggered}
61 | {item.isDelayable &&
Last Executed: {item.lastExecuted}
} 62 | {item.isDelayable &&
Next Scheduled: {item.nextScheduled}
} 63 |
64 |
65 |
Trigger IDs:{renderStringArray(item.triggerIds)}
66 |
67 |
68 |
Additional:{renderStringArray(item.additionalEntitiesToTrack)}
69 |
70 |
71 |
72 |
73 | ); 74 | } 75 | 76 | export default AutomationListItem; -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/components/TraceItem.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion } from "react-bootstrap"; 2 | import { TraceDataResponse } from "../models/AutomationDetailResponse"; 3 | import LogEntry from "./LogEntry"; 4 | import icons from '../assets/icons/bootstrap-icons.svg'; 5 | 6 | 7 | interface Props { 8 | trace: TraceDataResponse; 9 | index: number 10 | } 11 | 12 | function TraceItem(props: Props) { 13 | const errorIconSize: number = 32; 14 | 15 | return ( 16 | 17 | 18 |
19 |
Time: {new Date(props.trace.event.eventTime).toLocaleString()}
20 |
Type: {props.trace.event.eventType}
21 |
22 | Log Count: {props.trace.logs.length} 23 | {props.trace.event.exception &&
24 | 25 | 26 | 27 |
} 28 |
29 |
30 |
31 | 32 | {props.trace.event.stateChange && <> 33 |
34 |
Entity: {props.trace.event.stateChange.entityId}
35 |
Old: {props.trace.event.stateChange.old?.state ?? "null"}
36 |
New: {props.trace.event.stateChange.new.state}
37 |
38 | 39 |
40 |