├── 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