├── src ├── HaKafkaNet.UI │ ├── .env │ ├── .env.production │ ├── .env.development │ ├── src │ │ ├── vite-env.d.ts │ │ ├── assets │ │ │ └── hkn_128.png │ │ ├── App.css │ │ ├── models │ │ │ ├── SystemInfo.ts │ │ │ ├── AutomationData.ts │ │ │ └── AutomationDetailResponse.ts │ │ ├── main.tsx │ │ ├── App.tsx │ │ └── components │ │ │ ├── AutomationList.tsx │ │ │ ├── TraceItem.tsx │ │ │ └── AutomationListItem.tsx │ ├── public │ │ └── favicon.ico │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ └── README.md ├── HaKafkaNet │ ├── Models │ │ ├── EntityModels │ │ │ ├── SunExtensions.cs │ │ │ ├── TagAttributes.cs │ │ │ ├── MediaPlayer.cs │ │ │ ├── Lock.cs │ │ │ ├── CommonEnums.cs │ │ │ ├── CalendarModel.cs │ │ │ ├── HaAutomationModel.cs │ │ │ ├── BaseEntityModel.cs │ │ │ ├── ClimateEnums.cs │ │ │ ├── HaEntityStateChange.cs │ │ │ ├── SceneControllerEvent.cs │ │ │ ├── GeoLocation.cs │ │ │ ├── Sun.cs │ │ │ ├── Weather.cs │ │ │ ├── LightModel.cs │ │ │ └── LightProps.cs │ │ ├── HaApiModels │ │ │ ├── NotificationAction.cs │ │ │ ├── PiperSettings.cs │ │ │ ├── RokuCommands.cs │ │ │ ├── NotificationCommand.cs │ │ │ ├── HaNotification.cs │ │ │ └── Bytes.cs │ │ ├── BadEntityState.cs │ │ ├── HaKafkaNetException.cs │ │ ├── TraceData.cs │ │ ├── JsonConverters │ │ │ ├── GlobalConverters.cs │ │ │ └── HaDateTimeConverter.cs │ │ ├── TraceEvent.cs │ │ ├── EventTiming.cs │ │ └── HaKafkaNetConfig.cs │ ├── www │ │ ├── favicon.ico │ │ ├── assets │ │ │ └── hkn_128-f6PbFPlS.png │ │ └── index.html │ ├── nugetAssets │ │ ├── hkn_128.png │ │ └── readme.md │ ├── API │ │ ├── GetAutomations │ │ │ ├── AutomationListResponse.cs │ │ │ └── GetAutomationListEndpoint.cs │ │ ├── ApiResponse.cs │ │ ├── GetAutomationDetails │ │ │ ├── AutomationResponse.cs │ │ │ └── AutomationEndpoint.cs │ │ ├── NotifyStartupShutdown │ │ │ └── NotifyStartupShutdownEndpoint.cs │ │ ├── Notifications │ │ │ └── NotificationEndpoint.cs │ │ ├── GetSystemInfo │ │ │ ├── SystemInfoResponse.cs │ │ │ └── GetSystemInfoEndpoint.cs │ │ ├── PostEnableAutomation │ │ │ └── EnableAutomationEndpoint.cs │ │ └── GetErrorLog │ │ │ └── GetErrorLogEndpoint.cs │ ├── DI │ │ ├── ExcludeFromDiscoveryAttribute.cs │ │ └── OtelExtensions.cs │ ├── Implementations │ │ ├── AutomationBuilder │ │ │ ├── AutomationBuilderException.cs │ │ │ └── AutomationBuilder.cs │ │ ├── Automations │ │ │ ├── Wrappers │ │ │ │ ├── IAutomationWrapper.cs │ │ │ │ ├── TypedDelayedAutomationWrapper.cs │ │ │ │ └── TypedAutomationWrapper.cs │ │ │ ├── AutomationExtensions.cs │ │ │ ├── BaseAutomations │ │ │ │ ├── TypedAutomation.cs │ │ │ │ ├── DelayableAutomationBase.cs │ │ │ │ ├── SimpleAutomation.cs │ │ │ │ └── ConditionalAutomation.cs │ │ │ └── Prebuilt │ │ │ │ ├── LightOnMotionAutomation.cs │ │ │ │ └── LightOffOnNoMotion.cs │ │ ├── StartupHelpers.cs │ │ ├── Core │ │ │ ├── HknLogTarget.cs │ │ │ ├── AutomationActivator.cs │ │ │ └── UpdatingEntityProvider.cs │ │ └── Services │ │ │ ├── HaServices.cs │ │ │ ├── HaApiExtensions.cs │ │ │ └── HaEntityProvider.cs │ ├── KafkaHandlers │ │ └── HaMessageResolver.cs │ ├── Controllers │ │ └── HaKafkaNetController.cs │ ├── PublicInterfaces │ │ ├── IHaServices.cs │ │ ├── IStrongTypedAutomations.cs │ │ ├── IStartupHelpers.cs │ │ ├── AuxiliaryAutomationInterfaces.cs │ │ ├── IUpdatingEntityProvider.cs │ │ ├── IAutomationBuilder.cs │ │ ├── IHaEntityProvider.cs │ │ ├── IAutomationRegistry.cs │ │ ├── IHaStateCache.cs │ │ └── ISystemMonitor.cs │ ├── HaKafkaNet.sln │ └── Testing │ │ └── ServicesTestExtensions.cs └── HaKafkaNet.Tests │ ├── GlobalUsings.cs │ ├── HaKafkaNet.Tests.csproj │ └── Implementations │ ├── Models │ └── HaEntityStateConversionTests.cs │ ├── AutomationManagerTests │ ├── GetAllTests.cs │ └── GetByKeyTests.cs │ ├── Automations │ └── AutomationWrapperTests.cs │ └── HaEntityProviderTests.cs ├── example ├── HaKafkaNet.ExampleApp.Tests │ ├── GlobalUsings.cs │ ├── IntegrationTests │ │ ├── ActiveTests.cs │ │ └── LightOnRegistryTests.cs │ ├── HaKafkaNet.ExampleApp.Tests.csproj │ ├── Automations │ │ └── AutomationWithPreStartupTests.cs │ └── HaKafkanetFixture.cs ├── HaKafkaNet.ExampleApp │ ├── TestClasses │ │ ├── readme.md │ │ └── ActiveRegistry.cs │ ├── Dockerfile │ ├── Properties │ │ └── launchSettings.json │ ├── Automations │ │ ├── UpdatingEntityRegistry.cs │ │ ├── SceneControllerAutomation.cs │ │ ├── TemplateRegistry.cs │ │ ├── LightOnCustomAutomation.cs │ │ ├── MotionBehaviorTutorial.cs │ │ ├── SimpleLightAutomation.cs │ │ ├── ExampleDurableAutomation2.cs │ │ ├── ConditionalAutomationExample.cs │ │ ├── AutomationWithPreStartup.cs │ │ ├── AdvancedTutorialRegistry.cs │ │ ├── ExampleDurableAutomation.cs │ │ └── ExceptionTrowingAutomation.cs │ ├── HaKafkaNet.ExampleApp.csproj │ ├── appsettings.json │ └── Models │ │ └── AutoGen.cs └── docker-compose.yml ├── images ├── hkn.png ├── hkn_064.png ├── hkn_128.png ├── hkn_256.png ├── hkn_512.png ├── HaKafkaNetDashboard.png ├── HaKafkaNetDashboardV4.png ├── UI Examples │ ├── Menu-V5_5.PNG │ ├── LogDetails.PNG │ ├── Dashboard-V5_1.PNG │ ├── Dashboard-V5_2.PNG │ ├── Dashboard-V5_5.PNG │ ├── AutomationDetail-V5_1.PNG │ ├── AutomationDetail-V5_2.PNG │ ├── AutomationDetail-expanded-V5_3.PNG │ └── Dashboard-Detail-expanded-V5_2.PNG └── HKN Social media banner.png ├── .github └── workflows │ ├── example_build_test.yml │ ├── release_main.yml │ └── dotnet.yml ├── infrastructure ├── hakafkanet.jinja └── docker-compose.yml ├── LICENSE └── HaKafkaNet.sln /src/HaKafkaNet.UI/.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_URl='' -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/SunExtensions.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_API_URL=http://localhost:8082 -------------------------------------------------------------------------------- /src/HaKafkaNet.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Moq; 2 | global using Xunit; -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /images/hkn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/hkn.png -------------------------------------------------------------------------------- /images/hkn_064.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/hkn_064.png -------------------------------------------------------------------------------- /images/hkn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/hkn_128.png -------------------------------------------------------------------------------- /images/hkn_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/hkn_256.png -------------------------------------------------------------------------------- /images/hkn_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/hkn_512.png -------------------------------------------------------------------------------- /images/HaKafkaNetDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/HaKafkaNetDashboard.png -------------------------------------------------------------------------------- /src/HaKafkaNet/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/src/HaKafkaNet/www/favicon.ico -------------------------------------------------------------------------------- /images/HaKafkaNetDashboardV4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/HaKafkaNetDashboardV4.png -------------------------------------------------------------------------------- /images/UI Examples/Menu-V5_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/Menu-V5_5.PNG -------------------------------------------------------------------------------- /example/HaKafkaNet.ExampleApp/TestClasses/readme.md: -------------------------------------------------------------------------------- 1 | Items in this folder are used as a part of the build for HaKafkaNet itself. -------------------------------------------------------------------------------- /images/HKN Social media banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/HKN Social media banner.png -------------------------------------------------------------------------------- /images/UI Examples/LogDetails.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/LogDetails.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/Dashboard-V5_1.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/Dashboard-V5_2.PNG -------------------------------------------------------------------------------- /images/UI Examples/Dashboard-V5_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/Dashboard-V5_5.PNG -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/src/HaKafkaNet.UI/public/favicon.ico -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/assets/hkn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/src/HaKafkaNet.UI/src/assets/hkn_128.png -------------------------------------------------------------------------------- /src/HaKafkaNet/nugetAssets/hkn_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/src/HaKafkaNet/nugetAssets/hkn_128.png -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-V5_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/AutomationDetail-V5_1.PNG -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-V5_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/images/UI Examples/AutomationDetail-V5_2.PNG -------------------------------------------------------------------------------- /src/HaKafkaNet/www/assets/hkn_128-f6PbFPlS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/src/HaKafkaNet/www/assets/hkn_128-f6PbFPlS.png -------------------------------------------------------------------------------- /images/UI Examples/AutomationDetail-expanded-V5_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leosperry/ha-kafka-net/HEAD/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/HEAD/images/UI Examples/Dashboard-Detail-expanded-V5_2.PNG -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaApiModels/NotificationAction.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | public record NotificationAction(string action, string title, string? uri = null); 4 | -------------------------------------------------------------------------------- /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/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/BadEntityState.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | 4 | namespace HaKafkaNet; 5 | 6 | public record BadEntityState(string EntityId, HaEntityState? State = null); 7 | 8 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/models/SystemInfo.ts: -------------------------------------------------------------------------------- 1 | import { AutomationData } from "./AutomationData"; 2 | 3 | export interface SystemInfo { 4 | stateHandlerInitialized : boolean; 5 | version : string; 6 | } 7 | 8 | export interface AutomationListResponse { 9 | automations : AutomationData[] 10 | } 11 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetAutomations/AutomationListResponse.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public class AutomationListResponse 6 | { 7 | public required AutomationInfo[] Automations{ get; init; } 8 | } 9 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import 'bootstrap/dist/css/bootstrap.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/HaKafkaNet/DI/ExcludeFromDiscoveryAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Tells the discovery process at startup to ignore classes 5 | /// decorated with this and not create singletons 6 | /// 7 | public sealed class ExcludeFromDiscoveryAttribute: Attribute 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public record ApiResponse 6 | { 7 | public required Tdata Data { get; init; }} 8 | 9 | public record ApiResponse : ApiResponse 10 | { 11 | public Tmeta? Meta { get; init; } 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/AutomationBuilder/AutomationBuilderException.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Thrown when a builder does not have enough information provided to construct an automation 5 | /// 6 | public class AutomationBuilderException : Exception 7 | { 8 | internal AutomationBuilderException(string message): base(message){} 9 | } 10 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/models/AutomationData.ts: -------------------------------------------------------------------------------- 1 | export interface AutomationData { 2 | key : string; 3 | name : string; 4 | description : string; 5 | typeName : string; 6 | source : string; 7 | isDelayable : boolean; 8 | enabled : boolean; 9 | triggerIds : string[]; 10 | additionalEntitiesToTrack : string[]; 11 | lastTriggered : string; 12 | lastExecuted? : string; 13 | nextScheduled? : string; 14 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/TagAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace HaKafkaNet; 5 | 6 | public record TagAttributes 7 | { 8 | [JsonPropertyName("tag_id")] 9 | public required string TagId { get; set; } 10 | [JsonPropertyName("last_scanned_by_device_id")] 11 | public required string LastScannedByDeviceId { get; set; } 12 | [JsonPropertyName("friendly_name")] 13 | public required string FriendlyName { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaKafkaNetException.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Exception throw by HakafkaNet throughout the stack. 5 | /// Basic exception so that you can explicitly catch framework exceptions 6 | /// 7 | public class HaKafkaNetException : Exception 8 | { 9 | /// 10 | /// 11 | /// 12 | /// 13 | public HaKafkaNetException(string message): base(message) 14 | { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/MediaPlayer.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace HaKafkaNet; 7 | 8 | // https://www.home-assistant.io/integrations/media_player/ 9 | [JsonConverter(typeof(JsonStringEnumConverter))] 10 | public enum MediaPlayerState 11 | { 12 | Unknown, 13 | Unavailable, 14 | Off, 15 | On, 16 | Idle, 17 | Playing, 18 | Paused, 19 | Standby, 20 | Buffering 21 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/Lock.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace HaKafkaNet.Models.EntityModels; 7 | 8 | /// 9 | /// https://www.home-assistant.io/integrations/lock/ 10 | /// 11 | [JsonConverter(typeof(JsonStringEnumConverter))] 12 | public enum LockState 13 | { 14 | Unknown, 15 | Unavailable, 16 | Jammed, 17 | Open, 18 | Opening, 19 | Locked, 20 | Locking, 21 | Unlocking, 22 | } 23 | -------------------------------------------------------------------------------- /src/HaKafkaNet/KafkaHandlers/HaMessageResolver.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using KafkaFlow; 4 | using KafkaFlow.Middlewares.Serializer.Resolvers; 5 | 6 | namespace HaKafkaNet; 7 | 8 | public class HaMessageResolver : IMessageTypeResolver 9 | { 10 | public ValueTask OnConsumeAsync(IMessageContext context) 11 | { 12 | return ValueTask.FromResult(typeof(HaEntityState)); 13 | } 14 | 15 | public ValueTask OnProduceAsync(IMessageContext context) 16 | { 17 | return ValueTask.CompletedTask; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HaKafkaNet 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Controllers/HaKafkaNetController.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// wires up the physical files 9 | /// 10 | [Route("/hakafkanet/{*path}")] 11 | public class HaKafkaNetController : Controller 12 | { 13 | /// 14 | /// serves physical files 15 | /// 16 | /// 17 | public ActionResult Index() 18 | { 19 | var rootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 20 | var path = Path.Combine(rootPath, "www/index.html"); 21 | return PhysicalFile(path, "text/html"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/TraceData.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public record TraceData 6 | { 7 | public required TraceEvent TraceEvent { get; set; } 8 | public required IEnumerable Logs {get; set; } 9 | } 10 | 11 | public record LogInfo 12 | { 13 | public required string LogLevel { get; set; } 14 | public required string Message { get; set; } 15 | public string? RenderedMessage { get;set; } 16 | public IDictionary? Scopes { get; set; } 17 | public required IDictionary Properties { get; set; } 18 | public ExceptionInfo? Exception { get; set; } 19 | public DateTime? TimeStamp { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetAutomationDetails/AutomationResponse.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public record AutomationDetailResponse( 6 | string Name, 7 | string? Description, 8 | string KeyRequest, 9 | string GivenKey, 10 | string EventTimings, 11 | string Mode, 12 | IEnumerable TriggerIds, 13 | IEnumerable AdditionalEntities, 14 | string Type, 15 | string Source, 16 | bool IsDelayable, 17 | string LastTriggered, 18 | string? LastExecuted, 19 | IEnumerable Traces 20 | ); 21 | 22 | public record AutomationTraceResponse( 23 | TraceEvent Event, 24 | IEnumerable Logs 25 | ); -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IHaServices.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// a collectin of services for working with Home Assistant and entities 5 | /// 6 | public interface IHaServices 7 | { 8 | /// 9 | /// Provides method for interacting with HA REST API 10 | /// 11 | public IHaApiProvider Api { get; } 12 | 13 | /// 14 | /// Provides methods for working your user provided IDistributedCache 15 | /// 16 | public IHaStateCache Cache { get; } 17 | 18 | /// 19 | /// Provides methods that attempt to fetch entity state from the cache 20 | /// and fall back to the HA API 21 | /// 22 | public IHaEntityProvider EntityProvider { get; } 23 | } 24 | -------------------------------------------------------------------------------- /src/HaKafkaNet/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HaKafkaNet 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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/Models/EntityModels/CommonEnums.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | [JsonConverter(typeof(JsonStringEnumConverter))] 8 | public enum OnOff 9 | { 10 | Unknown, 11 | Unavailable, 12 | On, 13 | Off 14 | } 15 | 16 | [JsonConverter(typeof(JsonStringEnumConverter))] 17 | public enum BatteryState 18 | { 19 | Unknown, 20 | Unavailable, 21 | Charging, 22 | Discharging, 23 | Not_Charging 24 | } 25 | 26 | /// 27 | /// For use with media players 28 | /// 29 | [JsonConverter(typeof(JsonStringEnumConverter))] 30 | public enum Repeat 31 | { 32 | Off, 33 | All, 34 | One 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaApiModels/PiperSettings.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// https://github.com/home-assistant/addons/blob/master/piper/DOCS.md 9 | /// 10 | public record PiperSettings 11 | { 12 | [JsonPropertyName("voice")] 13 | public string? Voice { get; set; } 14 | 15 | [JsonPropertyName("speaker")] 16 | public int Speaker { get; set; } 17 | 18 | [JsonPropertyName("length_scale")] 19 | public float? LengthScale { get; set; } 20 | 21 | [JsonPropertyName("noise_scale")] 22 | public float? NoiseScale { get; set; } 23 | 24 | [JsonPropertyName("noise_w")] 25 | public float? SpeakingCadence { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/CalendarModel.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | public record CalendarModel : BaseEntityModel 8 | { 9 | [JsonPropertyName("message")] 10 | public string? Message { get; set; } 11 | 12 | [JsonPropertyName("description")] 13 | public string? Description { get; set; } 14 | 15 | [JsonPropertyName("all_day")] 16 | public bool? AllDay { get; set; } 17 | 18 | [JsonPropertyName("start_time")] 19 | public DateTime? StartTime { get; set; } 20 | 21 | [JsonPropertyName("end_time")] 22 | public DateTime? EndTime { get; set; } 23 | 24 | [JsonPropertyName("location")] 25 | public string? Location { get; set; } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaApiModels/RokuCommands.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | /// 6 | /// https://www.home-assistant.io/integrations/roku/ 7 | /// 8 | public enum RokuCommands 9 | { 10 | back, 11 | backspace, 12 | channel_down, 13 | channel_up, 14 | down, 15 | enter, 16 | find_remote, 17 | forward, 18 | home, 19 | info, 20 | input_av1, 21 | input_hdmi1, 22 | input_hdmi2, 23 | input_hdmi3, 24 | input_hdmi4, 25 | input_tuner, 26 | left, 27 | literal, 28 | play, 29 | power, 30 | replay, 31 | reverse, 32 | right, 33 | search, 34 | select, 35 | up, 36 | volume_down, 37 | volume_mute, 38 | volume_up 39 | } 40 | -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IStrongTypedAutomations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet; 5 | 6 | /// 7 | /// useful for scene controllers 8 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Scene-Controllers 9 | /// 10 | public interface IAutomation_SceneController : IAutomation; 11 | 12 | /// 13 | /// Great for creating virtual lights 14 | /// 15 | public interface IAutomation_ColorLight : IAutomation; 16 | 17 | /// 18 | /// great for light groups 19 | /// 20 | public interface IAutomation_DimmableLight : IAutomation; 21 | 22 | /// 23 | /// becase the state of a button is non-obvious 24 | /// 25 | public interface IAutomation_Button: IAutomation; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/HaAutomationModel.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | public record HaAutomationModel : BaseEntityModel 8 | { 9 | [JsonPropertyName("id")] 10 | public string? ID { get; set; } 11 | 12 | [JsonPropertyName("last_triggered")] 13 | public DateTime? LastTriggered { get; set; } 14 | 15 | [JsonPropertyName("mode")] 16 | [JsonConverter(typeof(JsonStringEnumConverter))] 17 | public HaAutomationMode Mode { get; set; } 18 | 19 | [JsonPropertyName("current")] 20 | public int Current { get; set; } 21 | } 22 | 23 | [JsonConverter(typeof(JsonStringEnumConverter))] 24 | public enum HaAutomationMode 25 | { 26 | Single, Restart, Queued, Parallel 27 | } 28 | -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IStartupHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | 4 | /// 5 | /// Collection of services best used at startup 6 | /// 7 | public interface IStartupHelpers 8 | { 9 | /// 10 | /// provides methods for quickly building automations 11 | /// 12 | public IAutomationBuilder Builder { get; } 13 | 14 | /// 15 | /// a small number of prebuilt automations 16 | /// 17 | public IAutomationFactory Factory { get; } 18 | 19 | /// 20 | /// provides entities that automatically updates as their state changes 21 | /// best used for things that don't update often and/or when millisecond timing is not critical 22 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Updating-Entity-Provider 23 | /// 24 | public IUpdatingEntityProvider UpdatingEntityProvider { get; } 25 | } 26 | -------------------------------------------------------------------------------- /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"] -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/Wrappers/IAutomationWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaKafkaNet; 4 | 5 | internal interface IAutomationWrapperBase : IInitializeOnStartup 6 | { 7 | IAutomationBase WrappedAutomation { get; } 8 | 9 | Task IInitializeOnStartup.Initialize() 10 | { 11 | IAutomationBase target = GetRoot(); 12 | return (target as IInitializeOnStartup)?.Initialize() ?? Task.CompletedTask; 13 | } 14 | 15 | IAutomationBase GetRoot() 16 | { 17 | IAutomationBase target = WrappedAutomation; 18 | while (target is IAutomationWrapperBase wrapped) 19 | { 20 | target = wrapped.WrappedAutomation; 21 | } 22 | return target; 23 | } 24 | } 25 | 26 | /// 27 | /// This is the one all automations come back to 28 | /// 29 | internal interface IAutomationWrapper : IAutomation, IAutomationWrapperBase, IAutomationMeta; 30 | 31 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/AuxiliaryAutomationInterfaces.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaKafkaNet; 4 | 5 | /// 6 | /// tells the framework additional logic should be called at startup 7 | /// 8 | public interface IInitializeOnStartup 9 | { 10 | /// 11 | /// called before automations begin running 12 | /// 13 | /// 14 | Task Initialize(); 15 | } 16 | 17 | /// 18 | /// Tells the framework that the use will supply metadata 19 | /// 20 | public interface IAutomationMeta 21 | { 22 | /// 23 | /// 24 | /// 25 | /// 26 | AutomationMetaData GetMetaData(); 27 | } 28 | 29 | /// 30 | /// 31 | /// 32 | public interface ISetAutomationMeta 33 | { 34 | /// 35 | /// 36 | /// 37 | /// 38 | void SetMeta(AutomationMetaData meta); 39 | } 40 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/NotifyStartupShutdown/NotifyStartupShutdownEndpoint.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | using FastEndpoints; 5 | 6 | namespace HaKafkaNet; 7 | 8 | internal class NotifyStartupShutdownEndpoint : Endpoint 9 | { 10 | ISystemObserver _observer; 11 | 12 | public NotifyStartupShutdownEndpoint(ISystemObserver observer) 13 | { 14 | _observer = observer; 15 | } 16 | 17 | public override void Configure() 18 | { 19 | Post("api/notifystartupshutdown"); 20 | AllowAnonymous(); 21 | } 22 | 23 | public override Task ExecuteAsync(StartUpShutDownEvent req, CancellationToken ct) 24 | { 25 | _observer.OnHaStartUpShutdown(req, ct); 26 | return Task.FromResult(Response); 27 | } 28 | } 29 | 30 | public record StartUpShutDownEvent 31 | { 32 | public string? Event { get; set; } 33 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/BaseEntityModel.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// https://developers.home-assistant.io/docs/core/entity 9 | /// 10 | public abstract record BaseEntityModel 11 | { 12 | [JsonPropertyName("friendly_name")] 13 | public string? FriendlyName { get; init; } 14 | 15 | [JsonPropertyName("icon")] 16 | public string? Icon { get; init; } 17 | 18 | [JsonPropertyName("supported_features")] 19 | public int? SupportedFeatures { get; init; } 20 | } 21 | 22 | public record DeviceModel : BaseEntityModel 23 | { 24 | [JsonPropertyName("device_class")] 25 | public string? DeviceClass { get; init; } 26 | } 27 | 28 | public record SensorModel : DeviceModel 29 | { 30 | [JsonPropertyName("unit_of_measurement")] 31 | public string? UnitOfMeasurement { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/Notifications/NotificationEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace HaKafkaNet; 5 | 6 | internal class NotificationEndpoint : Endpoint 7 | { 8 | readonly ISystemObserver _observer; 9 | readonly ILogger _logger; 10 | 11 | public NotificationEndpoint(ISystemObserver observer, ILogger logger) 12 | { 13 | _observer = observer; 14 | _logger = logger;; 15 | } 16 | 17 | public override void Configure() 18 | { 19 | Post("api/notification"); 20 | AllowAnonymous(); 21 | } 22 | 23 | public override Task ExecuteAsync(HaNotification req, CancellationToken ct) 24 | { 25 | _logger.LogTrace("Received notification {notification_id} {op}", req.Id, req.UpdateType); 26 | _observer.OnHaNotification(req, ct); 27 | 28 | return Task.FromResult(Response); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaApiModels/NotificationCommand.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | /// 6 | /// https://companion.home-assistant.io/docs/notifications/notification-commands 7 | /// 8 | public enum AndroidCommand 9 | { 10 | clear_notification, 11 | command_activity, 12 | command_app_lock, 13 | command_auto_screen_brightness, 14 | command_bluetooth, 15 | command_ble_transmitter, 16 | command_beacon_monitor, 17 | command_broadcast_intent, 18 | command_dnd, 19 | command_high_accuracy_mode, 20 | command_launch_app, 21 | command_media, 22 | command_ringer_mode, 23 | command_screen_brightness_level, 24 | command_screen_off_timeout, 25 | command_screen_on, 26 | command_stop_tts, 27 | command_persistent_connection, 28 | command_update_sensors, 29 | command_volume_level, 30 | command_webview, 31 | remove_channel, 32 | request_location_update 33 | } 34 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/ClimateEnums.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace HaKafkaNet; 7 | 8 | [JsonConverter(typeof(JsonStringEnumConverter))] 9 | public enum HvacMode 10 | { 11 | Unknown, 12 | Unavailable, 13 | Off, 14 | Heat, 15 | Cool, 16 | [JsonPropertyName("heat_cool")] 17 | HeatCool, 18 | Auto, 19 | Dry, 20 | [JsonPropertyName("fan_only")] 21 | FanOnly 22 | } 23 | 24 | [JsonConverter(typeof(JsonStringEnumConverter))] 25 | public enum CarrierFanMode 26 | { 27 | Unknown, 28 | Unavailable, 29 | Low, 30 | med, 31 | High, 32 | Auto 33 | } 34 | 35 | [JsonConverter(typeof(JsonStringEnumConverter))] 36 | public enum CarrierPresetMode 37 | { 38 | Unknown, 39 | Unavailable, 40 | Away, 41 | Home, 42 | manual, 43 | Sleep, 44 | wake, 45 | vacation, 46 | resume 47 | } -------------------------------------------------------------------------------- /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/Models/HaApiModels/HaNotification.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.ComponentModel; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// https://www.home-assistant.io/integrations/persistent_notification/ 9 | /// 10 | public record HaNotification 11 | { 12 | public string? Id { get; set; } 13 | public string? Title { get; set; } 14 | public string? Message { get; set; } 15 | public string? UpdateType { get; set; } 16 | } 17 | 18 | public static class HaNotificationExtensions 19 | { 20 | public static HaNotificationType? GetNotificationType(this HaNotification notification) 21 | { 22 | if (notification.UpdateType is null) 23 | { 24 | return null; 25 | } 26 | return Enum.Parse(notification.UpdateType, true); 27 | } 28 | } 29 | 30 | public enum HaNotificationType 31 | { 32 | Added, 33 | Removed, 34 | Updated, 35 | Current 36 | } 37 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetSystemInfo/SystemInfoResponse.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public record SystemInfoResponse 6 | { 7 | public bool StateHandlerInitialized { get; init; } 8 | public required string Version { get; init; } 9 | } 10 | 11 | public record AutomationInfo 12 | { 13 | public required string Key { get; set; } 14 | public required string Name { get; init; } 15 | public required string Description { get; set; } 16 | public required string TypeName { get; init; } 17 | public required string Source { get; set; } 18 | public required bool IsDelayable { get; set; } 19 | public required IEnumerable TriggerIds { get; init; } 20 | public required IEnumerable AdditionalEntitiesToTrack { get; set; } 21 | public bool Enabled { get; set; } 22 | public string? LastTriggered { get; set; } 23 | public string? LastExecuted { get; set; } 24 | public string? NextScheduled { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/HaEntityStateChange.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace HaKafkaNet; 3 | 4 | /// 5 | /// represents an entity changing state 6 | /// 7 | /// 8 | public record HaEntityStateChange 9 | { 10 | /// 11 | /// The timing assigned by the framework 12 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Event-Timings 13 | /// 14 | public required EventTiming EventTiming { get; set;} 15 | 16 | /// 17 | /// Id of the entity which changed state 18 | /// 19 | public required string EntityId { get; set; } 20 | 21 | /// 22 | /// The most recent item from the cache 23 | /// 24 | public T? Old { get ; set; } 25 | 26 | /// 27 | /// new state of the entity 28 | /// 29 | public required T New { get ; set; } 30 | } 31 | 32 | /// 33 | /// represents an entity changing state in raw form 34 | /// 35 | public record HaEntityStateChange : HaEntityStateChange; 36 | 37 | -------------------------------------------------------------------------------- /src/HaKafkaNet/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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaKafkaNet", "HaKafkaNet.csproj", "{C9AD2940-F918-478E-B094-38931660A48D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C9AD2940-F918-478E-B094-38931660A48D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C9AD2940-F918-478E-B094-38931660A48D}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C9AD2940-F918-478E-B094-38931660A48D}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C9AD2940-F918-478E-B094-38931660A48D}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {8843CD22-B07E-404C-A844-07F70D5AFBD5} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/StartupHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Collection of services best used at startup 5 | /// 6 | /// 7 | /// 8 | /// 9 | public class StartupHelpers(IAutomationBuilder builder, IAutomationFactory factory, IUpdatingEntityProvider updatingEntityProvider) : IStartupHelpers 10 | { 11 | /// 12 | /// provides methods for quickly building automations 13 | /// 14 | public IAutomationBuilder Builder { get => builder; } 15 | 16 | /// 17 | /// a small number of prebuilt automations 18 | /// 19 | public IAutomationFactory Factory { get => factory;} 20 | 21 | /// 22 | /// provides entities that automatically updates as their state changes 23 | /// best used for things that don't update often and/or when millisecond timing is not critical 24 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Updating-Entity-Provider 25 | /// 26 | public IUpdatingEntityProvider UpdatingEntityProvider { get => updatingEntityProvider;} 27 | } 28 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Core/HknLogTarget.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using NLog; 4 | using NLog.Common; 5 | using NLog.Config; 6 | using NLog.Layouts; 7 | using NLog.Targets; 8 | 9 | namespace HaKafkaNet 10 | { 11 | [Target("HknTarget")] 12 | public sealed class HknLogTarget: TargetWithContext 13 | { 14 | private IAutomationTraceProvider _trace; 15 | private readonly Layout _layout; 16 | 17 | public HknLogTarget(IAutomationTraceProvider traceProvider) 18 | { 19 | this._trace = traceProvider; 20 | base.IncludeScopeNested = true; 21 | base.IncludeScopeProperties = true; 22 | this._layout = new SimpleLayout("${longdate} | ${logger} | ${message}"); 23 | this.Name = "HaKafkaNet Target"; 24 | } 25 | 26 | protected override void Write(LogEventInfo logEvent) 27 | { 28 | var scoped = this.GetScopeContextProperties(logEvent); 29 | var rendered = base.RenderLogEvent(_layout, logEvent); 30 | _trace.AddLog(rendered, logEvent, scoped); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/JsonConverters/GlobalConverters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet.Models.JsonConverters; 6 | 7 | /// 8 | /// used throughout the framework for JSON serialization 9 | /// 10 | public class GlobalConverters 11 | { 12 | /// 13 | /// used throughout the framework for JSON serialization 14 | /// 15 | public static readonly JsonSerializerOptions StandardJsonOptions = new JsonSerializerOptions() 16 | { 17 | 18 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 19 | Converters = 20 | { 21 | new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower), 22 | new RgbConverter(), 23 | new RgbwConverter(), 24 | new RgbwwConverter(), 25 | new XyConverter(), 26 | new HsConverter(), 27 | new HaDateTimeConverter(), 28 | new HaNullableDateTimeConverter(), 29 | new HaDateOnlyConverter(), 30 | new HaNullableDateOnlyConverter() 31 | }, 32 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Services/HaServices.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Collection of services for working with HA and retrieving states 5 | /// 6 | public class HaServices : IHaServices 7 | { 8 | /// 9 | /// provides methods for calling HA API directly 10 | /// 11 | public IHaApiProvider Api { get; private set; } 12 | 13 | /// 14 | /// Provides methods for working with your provided IDistributedCache 15 | /// 16 | public IHaStateCache Cache { get; private set; } 17 | 18 | /// 19 | /// Provides entities by first going to cache and falling back to API 20 | /// 21 | public IHaEntityProvider EntityProvider { get; private set; } 22 | 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | public HaServices(IHaApiProvider api, IHaStateCache cache, IHaEntityProvider haEntityProvider) 30 | { 31 | this.Api = api; 32 | this.Cache = cache; 33 | this.EntityProvider = haEntityProvider; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetSystemInfo/GetSystemInfoEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | 3 | namespace HaKafkaNet; 4 | 5 | internal class GetSystemInfoEndpoint : EndpointWithoutRequest> 6 | { 7 | private readonly ISystemObserver _observer; 8 | private static readonly string _version; 9 | 10 | static GetSystemInfoEndpoint() 11 | { 12 | var ver = System.Reflection.Assembly.GetAssembly(typeof(IAutomation))?.GetName().Version; 13 | 14 | _version = ver?.ToString(3)!; 15 | } 16 | 17 | public GetSystemInfoEndpoint(ISystemObserver observer) 18 | { 19 | this._observer = observer; 20 | } 21 | 22 | public override void Configure() 23 | { 24 | Get("api/systeminfo"); 25 | AllowAnonymous(); 26 | } 27 | 28 | public override Task> ExecuteAsync(CancellationToken ct) 29 | { 30 | return Task.FromResult(new ApiResponse() 31 | { 32 | Data = new SystemInfoResponse() 33 | { 34 | StateHandlerInitialized = _observer.IsInitialized, 35 | Version = _version 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Models/TraceEvent.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | public record TraceEvent 6 | { 7 | public required DateTime EventTime { get; init; } 8 | public required string EventType { get; init; } 9 | public required string AutomationKey { get; init; } 10 | public HaEntityStateChange? StateChange { get; init; } 11 | public ExceptionInfo? Exception {get; set; } 12 | } 13 | 14 | public record ExceptionInfo 15 | { 16 | public required string Type { get; init; } 17 | public required string Message { get; init; } 18 | public string? StackTrace { get; init; } 19 | public ExceptionInfo? InnerException { get; init; } 20 | public IEnumerable? InnerExceptions { get; init; } 21 | 22 | public static ExceptionInfo Create(Exception ex) 23 | { 24 | return new ExceptionInfo() 25 | { 26 | Type = ex.GetType().FullName ?? ex.GetType().Name, 27 | Message = ex.Message, 28 | StackTrace = ex.StackTrace, 29 | InnerException = ex.InnerException is null ? null : Create(ex.InnerException), 30 | InnerExceptions = ex is AggregateException agg ? agg.InnerExceptions.Select(e => Create(e)) : null 31 | }; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/PostEnableAutomation/EnableAutomationEndpoint.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using FastEndpoints; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace HaKafkaNet; 7 | 8 | internal class EnableAutomationEndpoint : Endpoint 9 | { 10 | private readonly IAutomationManager _automationManager; 11 | private readonly ILogger _logger; 12 | 13 | public EnableAutomationEndpoint(IAutomationManager automationManager, ILogger logger) 14 | { 15 | this._automationManager = automationManager; 16 | this._logger = logger; 17 | } 18 | 19 | public override void Configure() 20 | { 21 | Post("/api/automation/enable"); 22 | AllowAnonymous(); 23 | } 24 | 25 | public override Task HandleAsync(EnableEndpointRequest req, CancellationToken ct) 26 | { 27 | if (_automationManager.EnableAutomation(req.Key, req.Enable)) 28 | { 29 | return SendOkAsync(ct); 30 | } 31 | return SendNotFoundAsync(ct); 32 | } 33 | } 34 | 35 | public class EnableEndpointRequest 36 | { 37 | //public Guid Id { get; set; } 38 | public required string Key { get; set; } 39 | public bool Enable { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/SceneControllerEvent.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// https://github.com/leosperry/ha-kafka-net/wiki/Scene-Controllers 9 | /// 10 | public record SceneControllerEvent : BaseEntityModel 11 | { 12 | [JsonPropertyName("event_types")] 13 | public string[]? EventTypes { get; set; } 14 | 15 | [JsonPropertyName("event_type")] 16 | public string? EventType { get; set; } 17 | } 18 | 19 | public static class EventModelExtensions 20 | { 21 | /// 22 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Scene-Controllers 23 | /// 24 | /// 25 | /// 26 | public static KeyPress? GetKeyPress(this SceneControllerEvent model) 27 | { 28 | if (model.EventType is not null && Enum.TryParse(model.EventType, out var keyPress)) 29 | { 30 | return keyPress; 31 | } 32 | return null; 33 | } 34 | } 35 | 36 | public enum KeyPress 37 | { 38 | KeyHeldDown, 39 | KeyPressed, 40 | KeyPressed2x, 41 | KeyPressed3x, 42 | KeyPressed4x, 43 | KeyPressed5x, 44 | KeyReleased 45 | } 46 | -------------------------------------------------------------------------------- /src/HaKafkaNet.UI/src/models/AutomationDetailResponse.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AutomationDetailsResponse { 3 | name : string; 4 | description: string; 5 | keyRequest: string; 6 | givenKey : string; 7 | eventTimings: string; 8 | mode: string; 9 | triggerIds : string[]; 10 | additionalEntities : string[]; 11 | type : string; 12 | source : string; 13 | isDelayable : boolean; 14 | lastTriggered : string; 15 | lastExecuted : string; 16 | traces : TraceDataResponse[]; 17 | } 18 | 19 | export interface TraceDataResponse { 20 | event : TraceEvent; 21 | logs : LogInfo[] 22 | } 23 | 24 | export interface TraceEvent { 25 | eventTime : string; 26 | eventType : string; 27 | automationKey : string; 28 | stateChange : StateChange; 29 | exception: object; 30 | } 31 | 32 | export interface LogInfo { 33 | logLevel : string; 34 | timeStamp? : Date; 35 | message : string; 36 | renderedMessage? : string; 37 | scopes : any; 38 | properties : any; 39 | exception : any; 40 | } 41 | 42 | export interface StateChange { 43 | entityId: string; 44 | old? : EntityState; 45 | new : EntityState; 46 | } 47 | 48 | export interface EntityState{ 49 | state : string; 50 | entity_id : string; 51 | last_changed : Date; 52 | last_updated : Date; 53 | context: object; 54 | attributes : object 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IUpdatingEntityProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet; 5 | 6 | /// 7 | /// Provides access to Entities that update in memory automatically. Use in moderation. 8 | /// Entities will perssist in memory until the application exits. 9 | /// Updates lock the entity during update. 10 | /// 11 | public interface IUpdatingEntityProvider 12 | { 13 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 14 | 15 | IUpdatingEntity GetEntity(string entityId); 16 | 17 | IUpdatingEntity GetEntity(string entityId) where Tstate: class; 18 | IUpdatingEntity GetEntity(string entityId) 19 | where Tstate : class 20 | where Tatt : class; 21 | 22 | IUpdatingEntity GetValueTypeEntity(string entityId) where Tstate: struct; 23 | IUpdatingEntity GetValueTypeEntity(string entityId) 24 | where Tstate: struct 25 | where Tatt : class; 26 | 27 | IUpdatingEntity GetEnumEntity(string entityId) 28 | where Tstate: System.Enum; 29 | IUpdatingEntity GetEnumEntity(string entityId) 30 | where Tstate: System.Enum 31 | where Tatt : class; 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/AutomationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// 5 | /// 6 | public static class AutomationExtensions 7 | { 8 | /// 9 | /// Adds metadata to an automation 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static T WithMeta(this T auto, string name, string? description = null, bool enabledAtStartup = true) 18 | where T: ISetAutomationMeta 19 | { 20 | AutomationMetaData meta = new() 21 | { 22 | Name = name, 23 | Description = description, 24 | Enabled = enabledAtStartup, 25 | }; 26 | auto.SetMeta(meta); 27 | return auto; 28 | } 29 | 30 | /// 31 | /// Adds metadata to an automation 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static T WithMeta(this T auto, AutomationMetaData meta) 38 | where T: ISetAutomationMeta 39 | { 40 | auto.SetMeta(meta); 41 | return auto; 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/PublicInterfaces/IAutomationBuilder.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 3 | 4 | using System.Text.Json; 5 | 6 | namespace HaKafkaNet; 7 | 8 | public interface IAutomationBuilder 9 | { 10 | SimpleAutomationBuildingInfo CreateSimple(bool enabledAtStartup = true); 11 | TypedAutomationBuildingInfo CreateSimple(bool enabledAtStartup = true); 12 | TypedAutomationBuildingInfo CreateSimple(bool enabledAtStartup = true) 13 | => CreateSimple(enabledAtStartup); 14 | 15 | ConditionalAutomationBuildingInfo CreateConditional(bool enabledAtStartup = true); 16 | TypedConditionalBuildingInfo CreateConditional(bool enabledAtStartup = true); 17 | TypedConditionalBuildingInfo CreateConditional(bool enabledAtStartup = true) 18 | => CreateConditional(enabledAtStartup); 19 | 20 | SchedulableAutomationBuildingInfo CreateSchedulable(bool reschedulable = false, bool enabledAtStartup = true); 21 | TypedSchedulableAutomationBuildingInfo CreateSchedulable(bool reschedulable = false, bool enabledAtStartup = true); 22 | TypedSchedulableAutomationBuildingInfo CreateSchedulable(bool reschedulable = false, bool enabledAtStartup = true) 23 | => CreateSchedulable(reschedulable, enabledAtStartup); 24 | 25 | SunAutomationBuildingInfo CreateSunAutomation(SunEventType sunEvent, bool enabledAtStartup = true); 26 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IHaEntityProvider.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace HaKafkaNet; 3 | 4 | /// 5 | /// Interface for retrieving entities with a strategy of 6 | /// first retrieving from the cache and using 7 | /// Home Assistant api as a backup 8 | /// 9 | public interface IHaEntityProvider : IEntityStateProvider 10 | { 11 | 12 | } 13 | 14 | /// 15 | /// provides methods for retrieving entities 16 | /// 17 | public interface IEntityStateProvider 18 | { 19 | /// 20 | /// Gets an entity 21 | /// 22 | /// 23 | /// 24 | /// 25 | Task GetEntity(string entityId, CancellationToken cancellationToken = default); 26 | 27 | /// 28 | /// Gets a strongly typed entity 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | Task?> GetEntity(string entityId, CancellationToken cancellationToken); 36 | 37 | /// 38 | /// Gets an entity typed as any use defined type 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | Task GetEntity(string entityId, CancellationToken cancellationToken = default) where T : class; 45 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/GeoLocation.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | public abstract record LatLongModel : BaseEntityModel 8 | { 9 | [JsonPropertyName("latitude")] 10 | public double Latitude { get; set; } 11 | 12 | [JsonPropertyName("longitude")] 13 | public double Longitude { get; set; } 14 | 15 | } 16 | 17 | public record ZoneModel : LatLongModel 18 | { 19 | [JsonPropertyName("radius")] 20 | public int radius { get; set; } 21 | 22 | [JsonPropertyName("passive")] 23 | public bool Passive { get; set; } 24 | 25 | [JsonPropertyName("persons")] 26 | public required string[] Persons { get; set; } 27 | } 28 | 29 | public abstract record TrackerModelBase: LatLongModel 30 | { 31 | [JsonPropertyName("gps_accuracy")] 32 | public int? GpsAccuracy { get; set; } 33 | } 34 | 35 | public record DeviceTrackerModel : TrackerModelBase 36 | { 37 | [JsonPropertyName("source_type")] 38 | public string? SourceType { get; set; } 39 | 40 | [JsonPropertyName("altitude")] 41 | public int? Altitude { get; set; } 42 | } 43 | 44 | public record PersonModel : TrackerModelBase 45 | { 46 | [JsonPropertyName("source")] 47 | public required string Source { get; set; } 48 | 49 | [JsonPropertyName("id")] 50 | public required string Id { get; set; } 51 | 52 | [JsonPropertyName("user_id")] 53 | public string? UserId { get; set; } 54 | 55 | [JsonPropertyName("device_trackers")] 56 | public string[]? DeviceTrackers { get; set; } 57 | } 58 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetErrorLog/GetErrorLogEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | 5 | namespace HaKafkaNet; 6 | 7 | internal record LogsRequest(string LogType); 8 | 9 | internal class GetErrorLogEndpoint : Endpoint< LogsRequest, Results>>,NotFound>> 10 | { 11 | readonly IAutomationTraceProvider _trace; 12 | 13 | public GetErrorLogEndpoint(IAutomationTraceProvider trace) 14 | { 15 | _trace = trace; 16 | } 17 | 18 | public override void Configure() 19 | { 20 | Get("api/log/{LogType}"); 21 | AllowAnonymous(); 22 | } 23 | 24 | public override async Task>>, NotFound>> ExecuteAsync(LogsRequest req, CancellationToken ct) 25 | { 26 | switch (req.LogType) 27 | { 28 | case "error": 29 | return TypedResults.Ok(new ApiResponse>() 30 | { 31 | Data = await _trace.GetErrorLogs() 32 | }); 33 | case "tracker": 34 | { 35 | return TypedResults.Ok(new ApiResponse>() 36 | { 37 | Data = await _trace.GetTrackerLogs() 38 | }); 39 | } 40 | case "global": 41 | return TypedResults.Ok(new ApiResponse>() 42 | { 43 | Data = await _trace.GetGlobalLogs() 44 | }); 45 | default: 46 | return TypedResults.NotFound(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/BaseAutomations/TypedAutomation.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | 5 | namespace HaKafkaNet; 6 | 7 | [ExcludeFromDiscovery] 8 | public class SimpleAutomation : IAutomation, IAutomationMeta, ISetAutomationMeta 9 | { 10 | private AutomationMetaData? _meta; 11 | private readonly Func>, CancellationToken, Task> _execute; 12 | public EventTiming EventTimings { get; protected internal set; } 13 | public bool IsActive { get; protected internal set;} 14 | private readonly string[] _triggers; 15 | 16 | public SimpleAutomation(IEnumerable triggers, Func>, CancellationToken, Task> execute, EventTiming eventTimings) 17 | { 18 | _triggers = triggers.ToArray(); 19 | _execute = execute; 20 | this.EventTimings = eventTimings; 21 | } 22 | 23 | public async Task Execute(HaEntityStateChange> stateChange, CancellationToken ct) 24 | { 25 | await _execute(stateChange, ct); 26 | } 27 | 28 | public IEnumerable TriggerEntityIds() => _triggers; 29 | 30 | public void SetMeta(AutomationMetaData meta) 31 | { 32 | _meta = meta; 33 | } 34 | 35 | public AutomationMetaData GetMetaData() 36 | { 37 | var thisType = this.GetType(); 38 | return _meta ??= new AutomationMetaData() 39 | { 40 | Name = thisType.Name, 41 | Description = thisType.FullName, 42 | Enabled = true, 43 | UnderlyingType = thisType.Name 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/Prebuilt/LightOnMotionAutomation.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace HaKafkaNet; 3 | 4 | /// 5 | /// A simple automation to turn on a light when motion is detected 6 | /// 7 | [ExcludeFromDiscovery] 8 | public class LightOnMotionAutomation : SimpleAutomationBase 9 | { 10 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 11 | 12 | private readonly List _motionSensors = new(); 13 | private readonly List _lights = new(); 14 | private readonly IHaServices _services; 15 | 16 | public LightOnMotionAutomation(IEnumerable motionSensor, IEnumerable light, IHaServices entityProvider) 17 | : base(motionSensor, EventTiming.PostStartup) 18 | { 19 | _motionSensors.AddRange(motionSensor); 20 | _lights.AddRange(light); 21 | this._services = entityProvider; 22 | } 23 | 24 | public override Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 25 | { 26 | if (stateChange.New.GetStateEnum() == OnOff.On) 27 | { 28 | //turn on any lights that are not 29 | return Task.WhenAll( 30 | from lightId in _lights 31 | select _services.EntityProvider.GetOnOffEntity(lightId, cancellationToken) 32 | .ContinueWith(t => 33 | t.Result!.State == OnOff.Off 34 | ? _services.Api.TurnOn(lightId, cancellationToken) 35 | : Task.CompletedTask 36 | , cancellationToken, TaskContinuationOptions.NotOnFaulted, TaskScheduler.Current) 37 | ); 38 | } 39 | return Task.CompletedTask; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/Sun.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | public record SunModel : HaEntityState 8 | { 9 | 10 | } 11 | 12 | [JsonConverter(typeof(JsonStringEnumConverter))] 13 | public enum SunState 14 | { 15 | Unknown, 16 | Unavailable, 17 | Above_Horizon, Below_Horizon 18 | } 19 | 20 | public record SunAttributes() 21 | { 22 | [JsonPropertyName("next_dawn")] 23 | public required DateTime NextDawn { get; init; } 24 | 25 | [JsonPropertyName("next_dusk")] 26 | public required DateTime NextDusk { get; init; } 27 | 28 | [JsonPropertyName("next_midnight")] 29 | public required DateTime NextMidnight { get; init; } 30 | 31 | [JsonPropertyName("next_noon")] 32 | public required DateTime NextNoon { get; init; } 33 | 34 | [JsonPropertyName("next_rising")] 35 | public required DateTime NextRising { get; init; } 36 | 37 | [JsonPropertyName("next_setting")] 38 | public required DateTime NextSetting { get; init; } 39 | 40 | [JsonPropertyName("elevation")] 41 | public required float Elevation { get; init; } 42 | 43 | [JsonPropertyName("azimuth")] 44 | public required float Azimuth { get; init; } 45 | 46 | [JsonPropertyName("rising")] 47 | public required bool Rising { get; init; } 48 | 49 | [JsonPropertyName("friendly_name")] 50 | public required string FriendlyName { get; init; } 51 | } 52 | 53 | /// 54 | /// Used by automation builder and ConsolidatedSunAutomation 55 | /// 56 | public enum SunEventType 57 | { 58 | Dawn, 59 | Rise, 60 | Noon, 61 | Set, 62 | Dusk, 63 | Midnight 64 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/DI/OtelExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | using OpenTelemetry.Metrics; 4 | using OpenTelemetry.Trace; 5 | 6 | static class Telemetry 7 | { 8 | public const string 9 | TraceApiName = "ha_kafka_net.ha_api", 10 | TraceCacheName = "ha_kafka_net.cache", 11 | TraceAutomationName = "ha_kafka_net.automation", 12 | TraceTrackerName = "ha_kafka_net.entity_tracker", 13 | MeterStateHandler = "ha_kafka_net.state_handler", 14 | MeterTracesName = "ha_hakfa_net.trace", 15 | MeterCacheName = "ha_kafka_net.cache_meter" 16 | ; 17 | } 18 | 19 | /// 20 | /// Extension methods for adding OTEL instrumentation 21 | /// 22 | public static class OtelExtensions 23 | { 24 | /// 25 | /// Adds tracing for automations 26 | /// 27 | /// 28 | /// 29 | public static TracerProviderBuilder AddHaKafkaNetInstrumentation(this TracerProviderBuilder trace) 30 | { 31 | trace 32 | .AddSource(Telemetry.TraceApiName) 33 | .AddSource(Telemetry.TraceCacheName) 34 | .AddSource(Telemetry.TraceAutomationName) 35 | .AddSource(Telemetry.TraceTrackerName); 36 | return trace; 37 | } 38 | 39 | /// 40 | /// Adds metrics for things like entity states and automation triggers 41 | /// 42 | /// 43 | /// 44 | public static MeterProviderBuilder AddHaKafkaNetInstrumentation(this MeterProviderBuilder meter) 45 | { 46 | meter 47 | .AddMeter(Telemetry.MeterStateHandler) 48 | .AddMeter(Telemetry.MeterTracesName) 49 | .AddMeter(Telemetry.MeterCacheName); 50 | return meter; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Services/HaApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text.Json; 4 | 5 | namespace HaKafkaNet; 6 | 7 | /// 8 | /// 9 | /// 10 | public static class HaApiExtensions 11 | { 12 | /// 13 | /// Sometimes and entity is non-responsive, but HA does not report an error. 14 | /// This method turns on an entity then verifies it turned on 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// true if the entity reports on after being told to turn on 20 | public static async Task TurnOnAndVerify(this IHaApiProvider api, string entityId, CancellationToken cancellationToken) 21 | { 22 | await api.TurnOn(entityId, cancellationToken); 23 | var apiResponse = await api.GetEntity>(entityId, cancellationToken); 24 | return !apiResponse.entityState.Bad() && apiResponse.entityState?.State == OnOff.On; 25 | } 26 | 27 | /// 28 | /// Sometimes and entity is non-responsive, but HA does not report an error. 29 | /// This method turns off an entity then verifies it turned off 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// true if the entity reports off after being told to turn off 35 | public static async Task TurnOffAndVerify(this IHaApiProvider api, string entityId, CancellationToken cancellationToken) 36 | { 37 | await api.TurnOff(entityId, cancellationToken); 38 | var apiResponse = await api.GetEntity>(entityId, cancellationToken); 39 | return !apiResponse.entityState.Bad() && apiResponse.entityState?.State == OnOff.Off; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/HaKafkaNet/Testing/ServicesTestExtensions.cs: -------------------------------------------------------------------------------- 1 | using KafkaFlow; 2 | using Microsoft.Extensions.Caching.Distributed; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.Options; 7 | using Microsoft.Extensions.Time.Testing; 8 | 9 | namespace HaKafkaNet.Testing 10 | { 11 | /// 12 | /// 13 | /// 14 | public static class ServicesTestExtensions 15 | { 16 | /// 17 | /// Configures the services collection for integration tests 18 | /// 19 | /// 20 | /// A fake or mock of the API provider 21 | /// Optional fake or mock cache. Defaults to a MemoryDistributedCache 22 | /// 23 | public static IServiceCollection ConfigureForIntegrationTests(this IServiceCollection services, 24 | IHaApiProvider apiProvider, IDistributedCache? cache = null) 25 | { 26 | ServicesExtensions._isTestMode = true; 27 | services 28 | .AddSingleton() 29 | .RemoveAll() 30 | .AddSingleton() 31 | .RemoveAll() 32 | .AddSingleton(cache ?? MakeCache()) 33 | .RemoveAll() 34 | .AddSingleton(apiProvider) 35 | .AddSingleton, HaStateHandler>(); 36 | 37 | return services; 38 | } 39 | 40 | static IDistributedCache MakeCache() 41 | { 42 | IOptions options = Options.Create(new MemoryDistributedCacheOptions()); 43 | var cache = new MemoryDistributedCache(options); 44 | return cache; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/PublicInterfaces/IAutomationRegistry.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | 3 | namespace HaKafkaNet; 4 | 5 | /// 6 | /// This is the registry users create implementations of 7 | /// 8 | public interface IAutomationRegistry 9 | { 10 | /// 11 | /// Called by the framework to register automations 12 | /// 13 | /// 14 | void Register(IRegistrar reg); 15 | } 16 | 17 | /// 18 | /// provides methods for registering automations 19 | /// 20 | public interface IRegistrar 21 | { 22 | /// 23 | /// original method used to register automations 24 | /// 25 | /// 26 | void Register(params IAutomation[] automations); 27 | 28 | /// 29 | /// registers strongly typed automations 30 | /// 31 | /// 32 | /// 33 | /// 34 | void Register(params IAutomation[] automations); 35 | 36 | /// 37 | /// registers delayable automations 38 | /// 39 | /// 40 | void RegisterDelayed(params IDelayableAutomation[] automations); 41 | 42 | /// 43 | /// registers different types of automations 44 | /// 45 | /// 46 | /// 47 | bool TryRegister(params IAutomationBase[] automations); 48 | 49 | /// 50 | /// USE THIS ONE 51 | /// Registers different types of automations and wraps their construction 52 | /// in a try/catch so that any failures do not prevent the rest of your 53 | /// automations from being constructed. 54 | /// 55 | /// 56 | /// 57 | bool TryRegister(params Func[] activators); 58 | } 59 | 60 | internal interface IInternalRegistrar : IRegistrar 61 | { 62 | IEnumerable Registered { get; } 63 | } 64 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/Wrappers/TypedDelayedAutomationWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaKafkaNet; 4 | 5 | internal abstract class TypedDelayedAutomationWrapper : IAutomationWrapperBase 6 | { 7 | public EventTiming EventTimings { get => WrappedAutomation.EventTimings;} 8 | public bool IsActive { get => WrappedAutomation.IsActive; } 9 | 10 | public abstract IAutomationBase WrappedAutomation { get; } 11 | } 12 | 13 | [ExcludeFromDiscovery] 14 | internal class TypedDelayedAutomationWrapper : TypedDelayedAutomationWrapper, IDelayableAutomation where Tauto: IDelayableAutomation 15 | { 16 | public bool ShouldExecutePastEvents { get => _automation.ShouldExecutePastEvents; } 17 | public bool ShouldExecuteOnContinueError { get => _automation.ShouldExecuteOnContinueError; } 18 | IDelayableAutomation _automation; 19 | private readonly ISystemObserver _observer; 20 | 21 | public TypedDelayedAutomationWrapper(Tauto automation, ISystemObserver observer) 22 | { 23 | _automation = automation; 24 | _observer = observer; 25 | } 26 | 27 | public override IAutomationBase WrappedAutomation => _automation; 28 | 29 | public async Task ContinuesToBeTrue(HaEntityStateChange stateChange, CancellationToken ct) 30 | { 31 | HaEntityStateChange> typed; 32 | try 33 | { 34 | typed = stateChange.ToTyped(); 35 | } 36 | catch (Exception ex) 37 | { 38 | _observer.OnAutomationTypeConversionFailure(ex, this._automation, stateChange, ct); 39 | if (this._automation is IFallbackExecution fallback) 40 | { 41 | await fallback.FallbackExecute(ex, stateChange, ct); 42 | } 43 | return false; 44 | } 45 | return await _automation.ContinuesToBeTrue(typed, ct); 46 | } 47 | 48 | public async Task Execute(CancellationToken ct) 49 | { 50 | await _automation.Execute(ct); 51 | } 52 | 53 | public IEnumerable TriggerEntityIds() 54 | { 55 | return _automation.TriggerEntityIds(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EventTiming.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Event-Timings 5 | /// 6 | [Flags] 7 | public enum EventTiming 8 | { 9 | /// 10 | /// Can be used in development to effectively disable an automation 11 | /// 12 | None = 0b000000, 13 | /// 14 | /// There is no corresponding cache entry for the state currently being handled 15 | /// 16 | PreStartupNotCached = 0b000001, 17 | /// 18 | /// State currently being handled happened before the most recent cache entry 19 | /// 20 | PreStartupPreLastCached = 0b000010, 21 | /// 22 | /// State currently being handled matches the most recent cache entry 23 | /// 24 | PreStartupSameAsLastCached = 0b000100, 25 | /// 26 | /// State currently being handled does not match the most recent cache entry, but occurred simultaneously with the cached entry 27 | /// (extreme edge case) 28 | /// 29 | PreStartupSameTimeLastCached = 0b001000, 30 | /// 31 | /// State currently being handled happened after the most recent cache entry 32 | /// 33 | PreStartupPostLastCached = 0b010000, 34 | /// 35 | /// Event happened after startup (may or may not be cached) 36 | /// 37 | PostStartup = 0b100000, 38 | /// 39 | /// Primarily used for schedulable events that need to survive restarts 40 | /// 41 | Durable = 0b110101, 42 | /// 43 | /// Used for making durable schedulable automations, but will not trigger event if item was not cached. 44 | /// For certain edge cases where the cache was wiped and compaction has not run, where old events may exist in topic 45 | /// 46 | DurableIfCached = 0b110100, 47 | /// 48 | /// When IAutomation.EventTiming is set to this value, all events will be passed to the IAutomation 49 | /// 50 | All = 0b111111 51 | } 52 | -------------------------------------------------------------------------------- /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/PublicInterfaces/IHaStateCache.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Methods for working with the user provided IDistributedCache 5 | /// 6 | public interface IHaStateCache : IEntityStateProvider 7 | { 8 | /// 9 | /// Gets a user saved object 10 | /// 11 | /// 12 | /// 13 | /// rethrow exception if JsonSerializer.Deserialize throws. 14 | /// 15 | /// 16 | Task GetUserDefinedObject(string key, bool throwOnDeserializeException = false, CancellationToken cancellationToken = default) where T: class; 17 | 18 | /// 19 | /// Storage for any user defined object 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | Task SetUserDefinedObject(string key, T item, CancellationToken cancellationToken = default) where T: class; 27 | 28 | /// 29 | /// Gets a user saved item. Useful for things like DateTime, int, etc. 30 | /// 31 | /// 32 | /// 33 | /// rethrow exception if T.Parse() throws 34 | /// 35 | /// 36 | Task GetUserDefinedItem(string key, bool throwOnParseException = false, CancellationToken cancellationToken = default) where T : IParsable; 37 | 38 | /// 39 | /// Storage for user defined items. Useful for things like DateTime, int, etc. 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | Task SetUserDefinedItem(string key, T item, CancellationToken cancellationToken = default) where T : IParsable; 47 | } 48 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Core/AutomationActivator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace HaKafkaNet.Implementations.Core; 5 | 6 | interface IAutomationActivator 7 | { 8 | Task Activate(IAutomationWrapper automation, CancellationToken cancellationToken); 9 | event Action? Activated; 10 | } 11 | 12 | internal class AutomationActivator : IAutomationActivator 13 | { 14 | private readonly IHaEntityProvider _entityProvider; 15 | private readonly ILogger _logger; 16 | 17 | public AutomationActivator(IHaEntityProvider entityProvider, ILogger logger) 18 | { 19 | this._entityProvider = entityProvider; 20 | this._logger = logger; 21 | } 22 | 23 | public event Action? Activated; 24 | 25 | public async Task Activate(IAutomationWrapper automation, CancellationToken cancellationToken) 26 | { 27 | // Get the most recent state of the trigger entities 28 | HaEntityState? mostRecent = null; 29 | await foreach (var item in GetEntities(automation.TriggerEntityIds())) 30 | { 31 | mostRecent = (mostRecent is null || item.LastUpdated > mostRecent.LastUpdated) ? item : mostRecent; 32 | } 33 | 34 | if (mostRecent is null) 35 | { 36 | _logger.LogCritical("Could not find state to activate automation with"); 37 | return; 38 | } 39 | 40 | OnActivated(mostRecent); 41 | } 42 | 43 | private async IAsyncEnumerable GetEntities(IEnumerable entityIds) 44 | { 45 | foreach (var id in entityIds) 46 | { 47 | var entity = await _entityProvider.GetEntity(id); 48 | if (entity is not null) 49 | { 50 | yield return entity; 51 | } 52 | else 53 | { 54 | _logger.LogWarning("Entity with id {id} not found", id); 55 | } 56 | } 57 | } 58 | 59 | private void OnActivated(HaEntityState state) 60 | { 61 | using (_logger.BeginScope("{state}", state)) 62 | _logger.LogDebug("Activating {activated_entity}", state.EntityId); 63 | Activated?.Invoke(state); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaKafkaNetConfig.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// configuration information for HaKafkNet 5 | /// 6 | public class HaKafkaNetConfig 7 | { 8 | HomeAssistantConnectionInfo _haConnection = new(); 9 | 10 | /// 11 | /// broker addresses sent to KafkaFlow 12 | /// 13 | public string[] KafkaBrokerAddresses { get; set; } = []; 14 | 15 | /// 16 | /// Topic to be used to read states 17 | /// 18 | public string KafkaTopic { get; set; } = "home_assistant_states"; 19 | 20 | /// 21 | /// Enables the KafkaFlow dashboard 22 | /// 23 | public bool ExposeKafkaFlowDashboard { get; set; } = true; 24 | 25 | /// 26 | /// Enables the HaKafkaNet dashboard 27 | /// 28 | public bool UseDashboard { get; set; } = false; 29 | 30 | /// 31 | /// required connection info for Home Assistant 32 | /// 33 | public HomeAssistantConnectionInfo HaConnectionInfo 34 | { 35 | get => _haConnection; set => _haConnection = value; 36 | } 37 | 38 | /// 39 | /// contains information for configuring Kafka consumer 40 | /// 41 | public StateHandlerConfig StateHandler { get; set; } = new(); 42 | } 43 | 44 | /// 45 | /// your Home Assistant connection information 46 | /// 47 | public class HomeAssistantConnectionInfo 48 | { 49 | /// 50 | /// Location of your Home Assistant instance 51 | /// 52 | public string BaseUri { get; set; } = "http://localhost:8123"; 53 | 54 | /// 55 | /// user defined long lived access token for Home Assistant 56 | /// 57 | public string AccessToken { get; set; } = ""; 58 | } 59 | 60 | /// 61 | /// see: https://farfetch.github.io/kafkaflow/docs/guides/consumers/ 62 | /// 63 | public class StateHandlerConfig 64 | { 65 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 66 | 67 | public string GroupId { get; set; }= "hakafkanet-consumer"; 68 | public int BufferSize { get; set; } = 5; 69 | public int WorkerCount { get; set; } = 5; 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/Weather.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace HaKafkaNet.Models.EntityModels; 7 | 8 | /// 9 | /// https://www.home-assistant.io/integrations/weather/ 10 | /// 11 | [JsonConverter(typeof(JsonStringEnumConverter))] 12 | public enum WeatherState 13 | { 14 | Unknown, 15 | Unavailable, 16 | [JsonPropertyName("clear-night")] 17 | ClearNight, 18 | Cloudy, 19 | Fog, 20 | Hail, 21 | Lighting, 22 | [JsonPropertyName("lightning-rainy")] 23 | LightningRainy, 24 | [JsonPropertyName("partlycloudy")] 25 | PartlyCloudy, 26 | Pouring, 27 | Rainy, 28 | Snowy, 29 | [JsonPropertyName("snowy-rainy")] 30 | SnowyRainy, 31 | Sunny, 32 | Windy, 33 | [JsonPropertyName("windy-variant")] 34 | WindyVariant, 35 | Exceptional 36 | } 37 | 38 | 39 | public record Weather 40 | { 41 | [JsonPropertyName("apparent_temperature")] 42 | public float? ApparentTemperature { get; set; } 43 | 44 | [JsonPropertyName("cloud_coverage")] 45 | public float? CloudCoverage { get; set; } 46 | 47 | [JsonPropertyName("dew_point")] 48 | public float? DewDoint { get; set; } 49 | 50 | [JsonPropertyName("humidity")] 51 | public float? Humidity { get; set; } 52 | 53 | [JsonPropertyName("precipitation_unit")] 54 | public string? PrecipitationUnit { get; set; } 55 | 56 | [JsonPropertyName("pressure")] 57 | public float? Pressure { get; set; } 58 | 59 | [JsonPropertyName("temperature")] 60 | public float? Temperature { get; set; } 61 | 62 | [JsonPropertyName("temperature_unit")] 63 | public string? TemperatureUnit { get; set; } 64 | 65 | [JsonPropertyName("uv_index")] 66 | public float? UV_Index { get; set; } 67 | 68 | [JsonPropertyName("visibility")] 69 | public float? Visibility { get; set; } 70 | 71 | [JsonPropertyName("wind_bearing")] 72 | public float? wind_bearing { get; set; } 73 | 74 | [JsonPropertyName("wind_gust_speed")] 75 | public float? WindGustSpeed { get; set; } 76 | 77 | [JsonPropertyName("wind_speed")] 78 | public float? WindSpeed { get; set; } 79 | 80 | [JsonPropertyName("wind_speed_unit")] 81 | public string? WindSpeedUnit { get; set; } 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/LightModel.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace HaKafkaNet; 6 | 7 | public record LightModel : DeviceModel 8 | { 9 | [JsonPropertyName("brightness")] 10 | public byte? Brightness { get; init; } 11 | 12 | [JsonPropertyName("color_mode")] 13 | public string? ColorMode { get; init; } 14 | 15 | /// 16 | /// deprecated, but common 17 | /// 18 | [JsonPropertyName("off_with_transition")] 19 | public bool? OffWithTransition { get; init; } 20 | } 21 | 22 | /// 23 | /// pulled from https://developers.home-assistant.io/docs/core/entity/light/ 24 | /// 25 | public record ColorLightModel : LightModel 26 | { 27 | [JsonPropertyName("color_temp_kelvin")] 28 | public int? TempKelvin { get; init; } 29 | 30 | [JsonPropertyName("effect")] 31 | public string? Effect { get; init; } 32 | 33 | [JsonPropertyName("effect_list")] 34 | public string[]? EffectList { get; init; } 35 | 36 | [JsonPropertyName("hs_color")] 37 | public HsColor? HsColor { get; init; } 38 | 39 | [JsonPropertyName("is_on")] 40 | public bool? IsOn { get; init; } 41 | 42 | [JsonPropertyName("max_color_temp_kelvin")] 43 | public int? MaxColorTempKelvin { get; init; } 44 | 45 | [JsonPropertyName("min_color_temp_kelvin")] 46 | public int? Min_ColorTempKelvin { get; init; } 47 | 48 | [JsonPropertyName("rgb_color")] 49 | public RgbTuple? RGB { get; init; } 50 | 51 | [JsonPropertyName("rgbw_color")] 52 | public RgbwTuple? RGBW { get; init; } 53 | 54 | [JsonPropertyName("rgbww_color")] 55 | public RgbwwTuple? RGBWW { get; init; } 56 | 57 | [JsonPropertyName("supported_color_modes")] 58 | public string[]? SupportedColorModes { get; init; } 59 | 60 | [JsonPropertyName("xy_color")] 61 | public XyColor? XyColor { get; init; } 62 | 63 | /// 64 | /// deprecated, but common 65 | /// 66 | [JsonPropertyName("min_mireds")] 67 | public int? MinMireds { get; init; } 68 | 69 | /// 70 | /// deprecated, but common 71 | /// 72 | [JsonPropertyName("max_mireds")] 73 | public int? MaxMireds { get; init; } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/HaKafkaNet/API/GetAutomationDetails/AutomationEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FastEndpoints; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | 5 | namespace HaKafkaNet; 6 | 7 | internal record AutomationDetailRequest(string Key); 8 | 9 | internal class AutomationEndpoint : Endpoint< AutomationDetailRequest, Results>,NotFound>> 10 | { 11 | readonly IAutomationManager _autoMgr; 12 | readonly IAutomationTraceProvider _trace; 13 | 14 | public AutomationEndpoint(IAutomationManager automationManager, IAutomationTraceProvider traceProvider) 15 | { 16 | _autoMgr = automationManager; 17 | _trace = traceProvider; 18 | } 19 | 20 | public override void Configure() 21 | { 22 | Get("api/automation/{Key}"); 23 | AllowAnonymous(); 24 | } 25 | 26 | public override async Task>, NotFound>> ExecuteAsync(AutomationDetailRequest req, CancellationToken ct) 27 | { 28 | Results>, NotFound> response; 29 | var auto = _autoMgr.GetByKey(req.Key); 30 | if (auto is null) 31 | { 32 | response = TypedResults.NotFound(); 33 | return response; 34 | } 35 | var meta = auto.GetMetaData(); 36 | var traces = (await _trace.GetTraces(req.Key)).Select(t => new AutomationTraceResponse(t.TraceEvent, t.Logs)); 37 | 38 | var autoResponse = new AutomationDetailResponse( 39 | meta.Name, 40 | meta.Description, 41 | meta.KeyRequest ?? "none", 42 | meta.GivenKey, 43 | auto.EventTimings.ToString(), 44 | meta.Mode.ToString(), 45 | auto.TriggerEntityIds(), 46 | meta.AdditionalEntitiesToTrack ?? Enumerable.Empty(), 47 | meta.UnderlyingType!, 48 | meta.Source ?? "source error", 49 | meta.IsDelayable, 50 | meta.LastTriggered?.ToLocalTime().ToString() ?? "never", 51 | meta.LastExecuted?.ToLocalTime().ToString(), 52 | traces 53 | ); 54 | 55 | response = TypedResults.Ok(new ApiResponse() 56 | { 57 | Data = autoResponse 58 | }); 59 | return response; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Core/UpdatingEntityProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Text.Json; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace HaKafkaNet.Implementations.Core; 7 | 8 | internal class UpdatingEntityProvider : IUpdatingEntityProvider 9 | { 10 | ISystemObserver _sysObserver; 11 | 12 | ConcurrentDictionary _instances = new(); 13 | 14 | public UpdatingEntityProvider(ISystemObserver systemObserver) 15 | { 16 | _sysObserver = systemObserver; 17 | } 18 | 19 | public IUpdatingEntity GetEntity(string entityId) 20 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 21 | 22 | public IUpdatingEntity GetEntity(string entityId) where Tstate : class 23 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 24 | 25 | public IUpdatingEntity GetEntity(string entityId) 26 | where Tstate : class 27 | where Tatt : class 28 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 29 | 30 | public IUpdatingEntity GetEnumEntity(string entityId) where Tstate : Enum 31 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 32 | 33 | public IUpdatingEntity GetEnumEntity(string entityId) 34 | where Tstate : Enum 35 | where Tatt : class 36 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 37 | 38 | public IUpdatingEntity GetValueTypeEntity(string entityId) where Tstate : struct 39 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 40 | 41 | public IUpdatingEntity GetValueTypeEntity(string entityId) 42 | where Tstate : struct 43 | where Tatt : class 44 | => (ThreadSafeEntity)_instances.GetOrAdd(entityId, Create); 45 | 46 | private ThreadSafeEntity Create(string entityId) 47 | { 48 | var retVal = new ThreadSafeEntity(entityId); 49 | _sysObserver.RegisterThreadSafeEntityUpdater(entityId, state => retVal.Set(() => (HaEntityState)state)); 50 | return retVal; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/BaseAutomations/DelayableAutomationBase.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 3 | 4 | namespace HaKafkaNet; 5 | 6 | public abstract class DelayableAutomationBase : IDelayableAutomation 7 | { 8 | public EventTiming EventTimings { get; protected internal set; } = EventTiming.PostStartup; 9 | public bool IsActive { get; protected internal set;} 10 | 11 | public bool ShouldExecutePastEvents { get; set; } 12 | public bool ShouldExecuteOnContinueError { get; set; } 13 | 14 | protected readonly IEnumerable _triggerEntities; 15 | 16 | public DelayableAutomationBase(IEnumerable triggerEntities, 17 | bool shouldExecutePastEvents = false, 18 | bool shouldExecuteOnError = false) 19 | { 20 | _triggerEntities = triggerEntities; 21 | ShouldExecutePastEvents = shouldExecutePastEvents; 22 | ShouldExecuteOnContinueError = shouldExecuteOnError; 23 | } 24 | 25 | public abstract Task ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken cancellationToken); 26 | 27 | public abstract Task Execute(CancellationToken cancellationToken); 28 | 29 | public IEnumerable TriggerEntityIds() => _triggerEntities; 30 | } 31 | 32 | public abstract class DelayableAutomationBase : IDelayableAutomation , IAutomationMeta, ISetAutomationMeta 33 | { 34 | readonly IEnumerable _triggers; 35 | public EventTiming EventTimings { get; protected internal set; } = EventTiming.PostStartup; 36 | public bool IsActive { get; protected internal set;} 37 | public bool ShouldExecutePastEvents { get; set; } = false; 38 | public bool ShouldExecuteOnContinueError { get; set; } = false; 39 | 40 | public DelayableAutomationBase(IEnumerable triggers) 41 | { 42 | _triggers = triggers; 43 | } 44 | 45 | public abstract Task ContinuesToBeTrue(HaEntityStateChange> stateChange, CancellationToken ct); 46 | 47 | public abstract Task Execute(CancellationToken ct); 48 | 49 | public IEnumerable TriggerEntityIds() => _triggers; 50 | 51 | private AutomationMetaData? _meta; 52 | public AutomationMetaData GetMetaData() 53 | { 54 | return _meta ??= AutomationMetaData.Create(this); 55 | } 56 | 57 | public void SetMeta(AutomationMetaData meta) 58 | { 59 | _meta = meta; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/Wrappers/TypedAutomationWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaKafkaNet; 4 | 5 | abstract class TypedAutomationWrapper 6 | { 7 | public abstract Type WrappedType{ get; } 8 | } 9 | 10 | [ExcludeFromDiscovery] 11 | internal class TypedAutomationWrapper : TypedAutomationWrapper, IAutomationWrapper where Tauto: IAutomation 12 | { 13 | public EventTiming EventTimings { get => _automation.EventTimings; } 14 | public bool IsActive { get => _automation.IsActive; } 15 | 16 | internal readonly IAutomation _automation; 17 | private readonly ISystemObserver _observer; 18 | 19 | private AutomationMetaData? _meta; 20 | 21 | public TypedAutomationWrapper(Tauto automation, ISystemObserver observer) 22 | { 23 | this._automation = automation; 24 | this._observer = observer; 25 | } 26 | 27 | public override Type WrappedType => _automation.GetType(); 28 | 29 | public IAutomationBase WrappedAutomation { get => _automation;} 30 | 31 | public async Task Execute(HaEntityStateChange stateChange, CancellationToken ct) 32 | { 33 | HaEntityStateChange> typed; 34 | try 35 | { 36 | typed = stateChange.ToTyped(); 37 | } 38 | catch (System.Exception ex) 39 | { 40 | _observer.OnAutomationTypeConversionFailure(ex, this.WrappedAutomation, stateChange, ct); 41 | if (this.WrappedAutomation is IFallbackExecution fallback) 42 | { 43 | await fallback.FallbackExecute(ex, stateChange, ct); 44 | } 45 | return; 46 | } 47 | 48 | await _automation.Execute(typed, ct); 49 | } 50 | 51 | public IEnumerable TriggerEntityIds() 52 | { 53 | return _automation.TriggerEntityIds(); 54 | } 55 | 56 | public AutomationMetaData GetMetaData() 57 | { 58 | return _meta ??= GetOrMakeMetaData(); 59 | } 60 | 61 | private AutomationMetaData GetOrMakeMetaData() 62 | { 63 | IAutomationMeta? autoImplementingMeta = _automation as IAutomationMeta; 64 | IAutomationBase target = _automation; 65 | while(autoImplementingMeta is null && target is IAutomationWrapperBase targetWrapper) 66 | { 67 | target = targetWrapper.WrappedAutomation; 68 | autoImplementingMeta = target as IAutomationMeta; 69 | } 70 | 71 | return autoImplementingMeta is null ? AutomationMetaData.Create(target) : autoImplementingMeta.GetMetaData(); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/nugetAssets/readme.md: -------------------------------------------------------------------------------- 1 | # HaKafkaNet 2 | A framework for creating Home Assistant automations in .NET and C#. 3 | 4 | Kafka ensures automations are durable and state is restored between restarts. 5 | 6 | It was created with the following goals: 7 | * Create Home Assistant automations in .NET / C# with abilities to: 8 | * track/retrieve states of all entities in Home Assistant 9 | * respond to Home Assistant state changes 10 | * call Home Assistant RESTful services 11 | * Enable all automation code to be fully testable with automated tests 12 | 13 | ## Why ha-kafka-net ? 14 | * Strongly typed [access to entities](https://github.com/leosperry/ha-kafka-net/wiki/State-Extension-Methods) 15 | * Strongly typed [automations](https://github.com/leosperry/ha-kafka-net/wiki/Automation-Types#generic-automations) 16 | * Durable - Automations that [survive restarts](https://github.com/leosperry/ha-kafka-net/wiki/Durable-Automations). See also [Event Timings](https://github.com/leosperry/ha-kafka-net/wiki/Event-Timings) 17 | * Fast - Automations run in parallel and asynchronously. 18 | * [UI](https://github.com/leosperry/ha-kafka-net/wiki/UI) to manage your automations and inspect Kafka consumers. 19 | * Observability through 20 | * [ISystemMonitor](https://github.com/leosperry/ha-kafka-net/wiki/System-Monitor) 21 | * [Tracing with log capturing](https://github.com/leosperry/ha-kafka-net/wiki/Tracing) 22 | * [Open Telemetry Instrumentation](https://github.com/leosperry/ha-kafka-net/wiki/Open-Telemetry-Instrumentation) 23 | * [Pre-built automations](https://github.com/leosperry/ha-kafka-net/wiki/Factory-Automations) 24 | * Extensible framework - [create your own reusable automations](https://github.com/leosperry/ha-kafka-net/wiki/Tutorial:-Creating-Automations) 25 | * Extend automation factory with extension methods 26 | * Create your own automations from scratch 27 | * [Automation builder](https://github.com/leosperry/ha-kafka-net/wiki/Automation-Builder) with fluent syntax for quickly creating automations. 28 | * Full unit testability for custom automations and integration testing for all registered integrations. 29 | * MIT license 30 | 31 | ## Example 32 | Example of multiple durable automations. See [Tutorial](https://github.com/leosperry/ha-kafka-net/wiki/Tutorial:-Creating-Automations) for more examples. 33 | ```csharp 34 | registrar.TryRegister( 35 | _factory.SunRiseAutomation( 36 | cancelToken => _api.TurnOff("light.night_light", cancelToken)), 37 | _factory.SunSetAutomation( 38 | cancelToken => _api.TurnOn("light.night_light", cancelToken), 39 | TimeSpan.FromMinutes(-10)) 40 | ); 41 | ``` 42 | 43 | See [Documentation](https://github.com/leosperry/ha-kafka-net/wiki) for full details. -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Services/HaEntityProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace HaKafkaNet; 4 | 5 | internal class HaEntityProvider : IHaEntityProvider 6 | { 7 | private readonly IHaStateCache _cache; 8 | private readonly IHaApiProvider _api; 9 | private readonly ILogger _logger; 10 | 11 | public HaEntityProvider(IHaStateCache cache, IHaApiProvider api, ILogger logger) 12 | { 13 | this._cache = cache; 14 | this._api = api; 15 | this._logger = logger; 16 | } 17 | 18 | public async Task GetEntity(string entityId, CancellationToken cancellationToken = default) 19 | { 20 | using (_logger.BeginScope("fetching entity {entity_id}", entityId)) 21 | { 22 | try 23 | { 24 | var cached = await _cache.GetEntity(entityId, cancellationToken); 25 | if (cached is not null) 26 | { 27 | return cached; 28 | } 29 | _logger.LogInformation("entity not found in cache"); 30 | } 31 | catch (System.Exception ex) 32 | { 33 | _logger.LogInformation(ex, "Error retrieving entity from cache"); 34 | } 35 | 36 | var apiReturn = await _api.GetEntity(entityId, cancellationToken); 37 | return apiReturn.entityState; 38 | } 39 | } 40 | 41 | public async Task GetEntity(string entityId, CancellationToken cancellationToken = default) where T : class 42 | { 43 | using (_logger.BeginScope("fetching entity {entity_id}", entityId)) 44 | { 45 | try 46 | { 47 | var cached = await _cache.GetEntity(entityId, cancellationToken); 48 | if (cached is not null) 49 | { 50 | return cached; 51 | } 52 | _logger.LogInformation("entity not found in cache"); 53 | } 54 | catch (System.Exception ex) 55 | { 56 | _logger.LogInformation(ex, "Error retrieving entity from cache"); 57 | } 58 | 59 | var apiReturn = await _api.GetEntity(entityId, cancellationToken); 60 | 61 | return apiReturn.entityState; 62 | } 63 | } 64 | 65 | public async Task?> GetEntity(string entityId, CancellationToken cancellationToken) 66 | { 67 | return await GetEntity>(entityId, cancellationToken); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/Prebuilt/LightOffOnNoMotion.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json; 3 | 4 | namespace HaKafkaNet; 5 | 6 | /// 7 | /// A conditional automation to turn lights off after a certain amount of time has passed with no motion 8 | /// 9 | [ExcludeFromDiscovery] 10 | public class LightOffOnNoMotion : ConditionalAutomationBase, IConditionalAutomation 11 | { 12 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 13 | 14 | private readonly List _motionIds = new(); 15 | private readonly List _lightIds = new(); 16 | private readonly IHaServices _services; 17 | 18 | public LightOffOnNoMotion(IEnumerable motionIds, IEnumerable lightIds, TimeSpan duration, IHaServices services) 19 | : base(motionIds.Union(lightIds), duration) 20 | { 21 | this._motionIds.AddRange(motionIds); 22 | this._lightIds.AddRange(lightIds); 23 | this._services = services; 24 | } 25 | 26 | 27 | public override Task ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken cancellationToken) 28 | { 29 | var motionStates = 30 | from m in _motionIds 31 | select _services.EntityProvider.GetOnOffEntity(m, cancellationToken); // all should be off 32 | 33 | var lightStates = 34 | from l in _lightIds 35 | select _services.EntityProvider.GetOnOffEntity(l, cancellationToken); // any should be on 36 | 37 | Task?[]> motionResults; 38 | Task?[]> lightResults; 39 | 40 | return Task.WhenAll( 41 | motionResults = Task.WhenAll(motionStates), 42 | lightResults = Task.WhenAll(lightStates)).ContinueWith(t => 43 | motionResults.Result.All(m => m?.State == OnOff.Off) && lightResults.Result.Any(l => l?.State == OnOff.On) 44 | ,cancellationToken, TaskContinuationOptions.NotOnFaulted, TaskScheduler.Current); 45 | } 46 | 47 | public override Task Execute(CancellationToken cancellationToken) 48 | { 49 | return Task.WhenAll( 50 | from lightId in _lightIds 51 | select _services.EntityProvider.GetOnOffEntity(lightId, cancellationToken) 52 | .ContinueWith(t => 53 | t.Result?.Bad() != true && t.Result?.State == OnOff.On 54 | ? _services.Api.TurnOff(lightId, cancellationToken) 55 | : Task.CompletedTask 56 | , cancellationToken, TaskContinuationOptions.NotOnFaulted, TaskScheduler.Current) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/EntityModels/LightProps.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 2 | 3 | namespace HaKafkaNet; 4 | 5 | /// 6 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#light-models 7 | /// 8 | /// 9 | /// 10 | /// 11 | public record RgbTuple(byte Red,byte Green, byte Blue) 12 | { 13 | public static implicit operator RgbTuple((byte red, byte green, byte blue) tuple) 14 | { 15 | return new RgbTuple(tuple.red, tuple.green, tuple.blue); 16 | } 17 | } 18 | 19 | /// 20 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#light-models 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | public record RgbwTuple(byte Red,byte Green, byte Blue, byte White) 27 | { 28 | public static implicit operator RgbwTuple((byte red, byte green, byte blue, byte white) tuple) 29 | { 30 | return new RgbwTuple(tuple.red, tuple.green, tuple.blue, tuple.white); 31 | } 32 | } 33 | 34 | /// 35 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#light-models 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | public record RgbwwTuple(byte Red,byte Green, byte Blue, byte White, byte WarmWhite) 43 | { 44 | public static implicit operator RgbwwTuple((byte red, byte green, byte blue, byte white, byte warmWhite) tuple) 45 | { 46 | return new RgbwwTuple(tuple.red, tuple.green, tuple.blue, tuple.white, tuple.warmWhite); 47 | } 48 | } 49 | 50 | /// 51 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#light-models 52 | /// 53 | /// 54 | /// 55 | public record XyColor(float X, float Y) 56 | { 57 | public static implicit operator XyColor((float x, float y) tuple) 58 | { 59 | return new XyColor(tuple.x, tuple.y); 60 | } 61 | } 62 | 63 | /// 64 | /// see: https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#light-models 65 | /// 66 | /// 67 | /// 68 | public record HsColor(float Hue, float Saturation) 69 | { 70 | public static implicit operator HsColor((float hue, float saturation) tuple) 71 | { 72 | return new HsColor(tuple.hue, tuple.saturation); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Implementations/AutomationBuilder/AutomationBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | internal class AutomationBuilder : IAutomationBuilder 4 | { 5 | private readonly TimeProvider _timeProvider; 6 | 7 | public AutomationBuilder(TimeProvider timeProvider) 8 | { 9 | _timeProvider = timeProvider; 10 | } 11 | 12 | public SimpleAutomationBuildingInfo CreateSimple(bool enabledAtStartup = true) 13 | { 14 | return new SimpleAutomationBuildingInfo() 15 | { 16 | TimeProvider = _timeProvider, 17 | EnabledAtStartup = enabledAtStartup 18 | }; 19 | } 20 | 21 | public TypedAutomationBuildingInfo CreateSimple(bool enabledAtStartup = true) 22 | { 23 | return new TypedAutomationBuildingInfo() 24 | { 25 | TimeProvider = _timeProvider, 26 | EnabledAtStartup = enabledAtStartup 27 | }; 28 | } 29 | 30 | public ConditionalAutomationBuildingInfo CreateConditional(bool enabledAtStartup = true) 31 | { 32 | return new ConditionalAutomationBuildingInfo() 33 | { 34 | TimeProvider = _timeProvider, 35 | EnabledAtStartup = enabledAtStartup 36 | }; 37 | } 38 | 39 | public SchedulableAutomationBuildingInfo CreateSchedulable(bool reschedulable = false, bool enabledAtStartup = true) 40 | { 41 | return new SchedulableAutomationBuildingInfo() 42 | { 43 | TimeProvider = _timeProvider, 44 | EnabledAtStartup = enabledAtStartup, 45 | IsReschedulable = reschedulable 46 | }; 47 | } 48 | 49 | public TypedConditionalBuildingInfo CreateConditional(bool enabledAtStartup = true) 50 | { 51 | return new TypedConditionalBuildingInfo() 52 | { 53 | TimeProvider = _timeProvider, 54 | EnabledAtStartup = enabledAtStartup 55 | }; 56 | } 57 | 58 | public SunAutomationBuildingInfo CreateSunAutomation(SunEventType sunEvent, bool enabledAtStartup = true) 59 | { 60 | return new SunAutomationBuildingInfo() 61 | { 62 | TimeProvider = _timeProvider, 63 | EnabledAtStartup = enabledAtStartup, 64 | SunEvent = sunEvent, 65 | Mode = AutomationMode.Parallel 66 | }; 67 | } 68 | 69 | public TypedSchedulableAutomationBuildingInfo CreateSchedulable(bool reschedulable = false, bool enabledAtStartup = true) 70 | { 71 | return new TypedSchedulableAutomationBuildingInfo() 72 | { 73 | TimeProvider = _timeProvider, 74 | EnabledAtStartup = enabledAtStartup, 75 | IsReschedulable = reschedulable 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Models/HaApiModels/Bytes.cs: -------------------------------------------------------------------------------- 1 | namespace HaKafkaNet; 2 | 3 | /// 4 | /// Utility class for working with Byte values 5 | /// see https://github.com/leosperry/ha-kafka-net/wiki/Utility-classes#bytes 6 | /// 7 | public static class Bytes 8 | { 9 | public const byte 10 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 11 | Zero = 0, 12 | One = 1, 13 | _1pct = 3, 14 | _5pct = 12, 15 | _10pct = 25, 16 | _15pct = 38, 17 | _20pct = 51, 18 | _25pct = 63, 19 | _30pct = 76, 20 | _33pct = 84, 21 | _35pct = 89, 22 | _40pct = 102, 23 | _45pct = 114, 24 | _50pct = 127, 25 | _55pct = 140, 26 | _60pct = 153, 27 | _65pct = 165, 28 | _67pct = 169, 29 | _70pct = 178, 30 | _75pct = 191, 31 | _80pct = 204, 32 | _85pct = 216, 33 | _90pct = 229, 34 | _95pct = 242, 35 | _100pct = 255, 36 | Max = 255; 37 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 38 | 39 | /// 40 | /// Converts a percentage to a byte 41 | /// 42 | /// an integer between 0 and 100 inclusive 43 | /// 44 | /// 45 | public static byte PercentToByte(int percent) 46 | { 47 | if (percent < 0 || percent > 100) 48 | { 49 | throw new ArgumentOutOfRangeException(nameof(percent), "percent must be between 0 and 100 inclusive"); 50 | } 51 | return (byte)(percent * 2.55f); 52 | } 53 | 54 | /// 55 | /// Converts a percentage to a byte 56 | /// 57 | /// a float between 0 and 1 inclusive 58 | /// 59 | /// 60 | public static byte PercentToByte(float percent) 61 | { 62 | if (percent < 0f || percent > 1f) 63 | { 64 | throw new ArgumentOutOfRangeException(nameof(percent), "percent must be between 0 and 1 inclusive"); 65 | } 66 | return (byte)(percent * 255f); 67 | } 68 | 69 | /// 70 | /// Converts a percentage to a byte 71 | /// 72 | /// a double between 0 and 1 inclusive 73 | /// 74 | /// 75 | public static byte PercentToByte(double percent) 76 | { 77 | if (percent < 0d || percent > 1d) 78 | { 79 | throw new ArgumentOutOfRangeException(nameof(percent), "percent must be between 0 and 1 inclusive"); 80 | } 81 | return (byte)(percent * 255d); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/HaKafkaNet/Implementations/Automations/BaseAutomations/SimpleAutomation.cs: -------------------------------------------------------------------------------- 1 | 2 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 3 | 4 | namespace HaKafkaNet; 5 | 6 | [ExcludeFromDiscovery] 7 | public abstract class SimpleAutomationBase : IAutomation, IAutomationMeta, ISetAutomationMeta 8 | { 9 | private readonly IEnumerable _triggerEntities; 10 | private AutomationMetaData? _meta; 11 | 12 | public SimpleAutomationBase(IEnumerable triggerEntities, EventTiming eventTimings) 13 | { 14 | this._triggerEntities = triggerEntities; 15 | this.EventTimings = eventTimings; 16 | } 17 | 18 | public EventTiming EventTimings { get; protected internal set; } 19 | public bool IsActive { get; protected internal set; } 20 | 21 | public abstract Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken); 22 | 23 | public virtual AutomationMetaData GetMetaData() 24 | { 25 | var thisType = this.GetType(); 26 | return _meta ??= new AutomationMetaData() 27 | { 28 | Name = thisType.Name, 29 | Description = thisType.FullName, 30 | Enabled = true, 31 | UnderlyingType = thisType.Name 32 | }; 33 | } 34 | 35 | public IEnumerable TriggerEntityIds() 36 | { 37 | return _triggerEntities; 38 | } 39 | 40 | public void SetMeta(AutomationMetaData meta) 41 | { 42 | _meta = meta; 43 | } 44 | } 45 | 46 | [ExcludeFromDiscovery] 47 | public class SimpleAutomation : SimpleAutomationBase 48 | { 49 | private readonly Func _execute; 50 | 51 | public SimpleAutomation(IEnumerable triggerEntities, Func execute, EventTiming eventTimings) 52 | :base(triggerEntities,eventTimings) 53 | { 54 | this._execute = execute; 55 | } 56 | 57 | public override Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 58 | { 59 | return _execute(stateChange, cancellationToken); 60 | } 61 | } 62 | 63 | [ExcludeFromDiscovery] 64 | public class SimpleAutomationWithServices : SimpleAutomationBase 65 | { 66 | IHaServices _services; 67 | private readonly Func _executeWithServices; 68 | 69 | public SimpleAutomationWithServices(IHaServices services, IEnumerable triggerEntities, Func execute, EventTiming eventTimings): 70 | base(triggerEntities, eventTimings) 71 | { 72 | _services = services; 73 | this._executeWithServices = execute; 74 | } 75 | 76 | public override Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken) 77 | { 78 | return _executeWithServices(_services, stateChange, cancellationToken); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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.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 |