├── .devcontainer ├── .bashrc ├── Dockerfile ├── devcontainer.json └── install_prettyprompt.sh ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── config.yml ├── dependabot.yml ├── matchers │ └── dotnet.json ├── release-drafter.yml ├── stale.yml └── workflows │ ├── check_pr_label.yml │ ├── ci_analyze.yml │ ├── ci_build.yml │ ├── common │ └── set_netdaemon_version.yml │ ├── push_docker_addon_manual.yml │ ├── push_docker_manual.yml │ ├── push_docker_prerelease.yml │ ├── push_nuget_prerelease.yml │ ├── release_drafter.yml │ ├── tags_docker.yml │ ├── tags_nuget.yml │ └── test_docker.yml ├── .gitignore ├── .linting ├── roslynator.config └── roslynator.ruleset ├── .vscode ├── daemon.code-snippets ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEV.md ├── Directory.Build.props ├── Docker ├── rootfs │ └── etc │ │ ├── s6-overlay │ │ └── s6-rc.d │ │ │ └── usr │ │ │ ├── .gitattributes │ │ │ ├── netdaemon │ │ │ └── netdaemon_addon │ │ └── services.d │ │ ├── netdaemon │ │ ├── .gitattributes │ │ ├── run │ │ └── type │ │ └── netdaemon_addon │ │ ├── .gitattributes │ │ ├── run │ │ └── type ├── run-nd.sh └── s6.sh ├── Dockerfile ├── Dockerfile.AddOn ├── LICENSE ├── NetDaemon.sln ├── README.md ├── codecov.yaml ├── global.json ├── img └── icon.png ├── src ├── AppModel │ ├── NetDaemon.AppModel.SourceDeployedApps │ │ ├── Compiler │ │ │ ├── CollectableAssemblyLoadContext.cs │ │ │ ├── CompileSettings.cs │ │ │ ├── Compiler.cs │ │ │ ├── ICompiler.cs │ │ │ ├── ISyntaxTreeResolver.cs │ │ │ └── SyntaxTreeResolver.cs │ │ ├── DynamicallyCompiledAppAssemblyProvider.cs │ │ ├── GlobalUsings.cs │ │ ├── NetDaemon.AppModel.SourceDeployedApps.csproj │ │ └── ServiceCollectionExtension.cs │ ├── NetDaemon.AppModel.Tests │ │ ├── .editorconfig │ │ ├── AppAssemblyProviders │ │ │ ├── CombinedAppAssemblyProviderTests.cs │ │ │ ├── DynamicAppAssemblyProviderTests.cs │ │ │ └── LocalAppAssemblyProviderTests.cs │ │ ├── AppFactories │ │ │ ├── DynamicAppFactoryTests.cs │ │ │ └── LocalAppFactoryTests.cs │ │ ├── AppFactoryProviders │ │ │ ├── CombinedAppFactoryProviderTests.cs │ │ │ ├── DynamicAppFactoryProviderTests.cs │ │ │ └── LocalAppFactoryProviderTests.cs │ │ ├── AppModelTests.cs │ │ ├── Compiler │ │ │ ├── CompilerIntegrationTests.cs │ │ │ └── Fixtures │ │ │ │ └── SimpleApp.cs │ │ ├── Config │ │ │ ├── ConfigTests.cs │ │ │ ├── ConfigurationBinderTests.cs │ │ │ ├── FailedConfig │ │ │ │ └── Fail.yaml │ │ │ └── Fixtures │ │ │ │ ├── App.json │ │ │ │ ├── App.yaml │ │ │ │ ├── AppInjectSettings.yaml │ │ │ │ ├── AppInjectSettingsy.json │ │ │ │ └── CollectionTestsFixture.yaml │ │ ├── Context │ │ │ ├── ApplicationContextTests.cs │ │ │ └── ApplicationScopeTests.cs │ │ ├── Fixtures │ │ │ ├── Dynamic │ │ │ │ ├── Application.cs │ │ │ │ └── settings.yaml │ │ │ ├── DynamicError │ │ │ │ └── IDoNotCompileWell.cs │ │ │ ├── DynamicWithFocus │ │ │ │ └── Application.cs │ │ │ ├── DynamicWithServiceCollection │ │ │ │ └── Application.cs │ │ │ ├── Local │ │ │ │ ├── AppThatTakesASlowTimeToDispose.cs │ │ │ │ ├── LocalApp.cs │ │ │ │ ├── LocalAppWithDisposable.cs │ │ │ │ ├── LocalAppWithId.cs │ │ │ │ ├── LocalAppWithInitializeAsync.cs │ │ │ │ └── settings.yaml │ │ │ └── LocalError │ │ │ │ └── LocalApp.cs │ │ ├── GlobalUsings.cs │ │ ├── Helpers │ │ │ ├── FakeOptions.cs │ │ │ ├── FakeOptionsExtensions.cs │ │ │ └── TestHelpers.cs │ │ └── NetDaemon.AppModel.Tests.csproj │ └── NetDaemon.AppModel │ │ ├── Common │ │ ├── ApplicationState.cs │ │ ├── Attributes │ │ │ ├── FocusAttribute.cs │ │ │ ├── JetBrainsCodeAnnotations.cs │ │ │ ├── NetDaemonAppAttribute.cs │ │ │ └── ServiceCollectionExtensionAttribute.cs │ │ ├── Extensions │ │ │ ├── IConfigurationBuilderExtensions.cs │ │ │ └── ServiceCollectionExtension.cs │ │ ├── IAppModel.cs │ │ ├── IAppModelContext.cs │ │ ├── IAppStateManager.cs │ │ ├── IApplication.cs │ │ ├── IAsyncInitializable.cs │ │ ├── IConfig.cs │ │ └── Settings │ │ │ └── AppLocationSettings.cs │ │ ├── GlobalUsings.cs │ │ ├── Internal │ │ ├── AppAssemblyProviders │ │ │ ├── AppAssemblyProvider.cs │ │ │ └── IAppAssemblyProvider.cs │ │ ├── AppFactories │ │ │ ├── FuncAppFactory.cs │ │ │ └── IAppFactory.cs │ │ ├── AppFactoryProviders │ │ │ ├── AssemblyAppFactoryProvider.cs │ │ │ ├── FuncAppFactoryProvider.cs │ │ │ ├── IAppFactoryProvider.cs │ │ │ └── SingleAppFactoryProvider.cs │ │ ├── AppModel.cs │ │ ├── AppModelContext.cs │ │ ├── Application.cs │ │ ├── Config │ │ │ ├── AppConfig.cs │ │ │ ├── BindingPoint.cs │ │ │ ├── ConfigurationBinder.cs │ │ │ ├── ConfigurationBinding.cs │ │ │ ├── IConfigurationBinding.cs │ │ │ ├── ParameterDefaultValue.cs │ │ │ └── Yaml │ │ │ │ ├── YamlConfigurationFileParser.cs │ │ │ │ ├── YamlConfigurationProvider.cs │ │ │ │ └── YamlConfigurationSource.cs │ │ ├── Context │ │ │ ├── ApplicationContext.cs │ │ │ └── ApplicationScope.cs │ │ └── FocusFilter.cs │ │ ├── NetDaemon.AppModel.csproj │ │ └── README.md ├── Client │ ├── NetDaemon.HassClient.Debug │ │ ├── GlobalUsings.cs │ │ ├── NetDaemon.HassClient.Debug.csproj │ │ ├── Program.cs │ │ ├── Service.cs │ │ ├── _appsettings.Development.json │ │ └── appsettings.json │ ├── NetDaemon.HassClient.Tests │ │ ├── .editorconfig │ │ ├── ExtensionsTest │ │ │ ├── HomeAssistantApiManagerExtensionTests.cs │ │ │ ├── JsonElementExtensionTest.cs │ │ │ ├── MqttEntityManagerTests │ │ │ │ ├── AssuredMqttConnectionTests.cs │ │ │ │ ├── ByteArrayHelperTests.cs │ │ │ │ ├── EntityCreationPayloadHelperTests.cs │ │ │ │ ├── EntityIdParserTests.cs │ │ │ │ ├── JsonNodeExtensionTests.cs │ │ │ │ ├── MessageSenderTests.cs │ │ │ │ ├── MqttClientOptionsFactoryTests.cs │ │ │ │ ├── MqttEntityManagerTester.cs │ │ │ │ └── TestHelpers │ │ │ │ │ └── MockMqttMessageSenderSetup.cs │ │ │ └── ServiceCollectionExtensionTests.cs │ │ ├── GlobalUsings.cs │ │ ├── HelperTest │ │ │ ├── HttpHandlerHelperTests.cs │ │ │ ├── ProgressiveTimoutTests.cs │ │ │ ├── ResultMessageHandlerTests.cs │ │ │ └── VersionHelperTests.cs │ │ ├── Helpers │ │ │ ├── LoggerMockExtensions.cs │ │ │ └── TestSettings.cs │ │ ├── HomeAssistantClientTest │ │ │ ├── HomeAssistantClientTests.cs │ │ │ ├── HomeAssistantConnectionMock.cs │ │ │ ├── HomeAssistantConnectionTests.cs │ │ │ └── TransportPipelineMock.cs │ │ ├── HomeAssistantRunnerTest │ │ │ ├── HomeAssistantClientMock.cs │ │ │ └── HomeAssistantRunnerTests.cs │ │ ├── Integration │ │ │ ├── ApiIntegrationTests.cs │ │ │ ├── HomeAssistantServerMock.cs │ │ │ ├── HomeAssistantServiceFixture.cs │ │ │ ├── IntegrationTestBase.cs │ │ │ ├── TestContext.cs │ │ │ ├── Testdata │ │ │ │ ├── auth_notok.json │ │ │ │ ├── auth_ok.json │ │ │ │ ├── auth_required.json │ │ │ │ ├── deviceregistry_update.json │ │ │ │ ├── event.json │ │ │ │ ├── pong.json │ │ │ │ ├── result_calendar_list_event.json │ │ │ │ ├── result_config.json │ │ │ │ ├── result_get_areas.json │ │ │ │ ├── result_get_devices.json │ │ │ │ ├── result_get_entities.json │ │ │ │ ├── result_get_floors.json │ │ │ │ ├── result_get_labels.json │ │ │ │ ├── result_get_services.json │ │ │ │ ├── result_msg.json │ │ │ │ ├── result_msg_error.json │ │ │ │ ├── result_states.json │ │ │ │ └── service_event.json │ │ │ └── WebsocketIntegrationTests.cs │ │ ├── Json │ │ │ ├── EnsureStringConverterTests.cs │ │ │ └── HassDeviceConverterTests.cs │ │ ├── Net │ │ │ ├── WebSocketClientFactoryTests.cs │ │ │ ├── WebSocketClientMock.cs │ │ │ └── WebSocketTransportPipelineTests.cs │ │ ├── NetDaemon.HassClient.Tests.csproj │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── README.md │ │ └── appsettings.json │ └── NetDaemon.HassClient │ │ ├── Common │ │ ├── DisconnectReason.cs │ │ ├── Exceptions │ │ │ ├── HomeAssistantApiCallException.cs │ │ │ └── HomeAssistantConnectionException.cs │ │ ├── Extensions │ │ │ └── ServiceCollectionExtension.cs │ │ ├── HomeAssistant │ │ │ ├── Extensions │ │ │ │ ├── HassEventExtensions.cs │ │ │ │ ├── HomeAssistantApiManagerExtensions.cs │ │ │ │ ├── HomeAssistantConnectionHelpersExtensions.cs │ │ │ │ └── IHomeAssistantConnectionExtensions.cs │ │ │ └── Model │ │ │ │ ├── CommandMessage.cs │ │ │ │ ├── HassArea.cs │ │ │ │ ├── HassAuthResponse.cs │ │ │ │ ├── HassConfig.cs │ │ │ │ ├── HassContext.cs │ │ │ │ ├── HassConversationOptions.cs │ │ │ │ ├── HassDevice.cs │ │ │ │ ├── HassEntity.cs │ │ │ │ ├── HassEntityOptions.cs │ │ │ │ ├── HassError.cs │ │ │ │ ├── HassEvent.cs │ │ │ │ ├── HassFloor.cs │ │ │ │ ├── HassLabel.cs │ │ │ │ ├── HassMessage.cs │ │ │ │ ├── HassMessageBase.cs │ │ │ │ ├── HassServiceEventData.cs │ │ │ │ ├── HassServiceResult.cs │ │ │ │ ├── HassState.cs │ │ │ │ ├── HassStateChangedEventData.cs │ │ │ │ ├── HassUnitSystem.cs │ │ │ │ ├── HassVariable.cs │ │ │ │ ├── InputBooleanHelper.cs │ │ │ │ ├── InputNumberHelper.cs │ │ │ │ └── Targets.cs │ │ ├── HomeAssistantClientConnector.cs │ │ ├── IHomeAssistantApiManager.cs │ │ ├── IHomeAssistantClient.cs │ │ ├── IHomeAssistantConnection.cs │ │ ├── IHomeAssistantRunner.cs │ │ └── Settings │ │ │ └── Settings.cs │ │ ├── GlobalUsings.cs │ │ ├── Internal │ │ ├── Extensions │ │ │ ├── CancellationExtensions.cs │ │ │ └── JsonExtensions.cs │ │ ├── Helpers │ │ │ ├── AsyncLazy.cs │ │ │ ├── HttpHelper.cs │ │ │ ├── ProgressiveTimeout.cs │ │ │ ├── ResultMessageHandler.cs │ │ │ └── VersionHelper.cs │ │ ├── HomeAssistant │ │ │ ├── Commands │ │ │ │ ├── CallExecuteScriptCommand.cs │ │ │ │ ├── CallServiceCommand.cs │ │ │ │ ├── CreateHelperCommandBase.cs │ │ │ │ ├── CreateInputBooleanHelperCommand.cs │ │ │ │ ├── CreateInputNumberCommand.cs │ │ │ │ ├── SimpleCommand.cs │ │ │ │ ├── SubscribeEventCommand.cs │ │ │ │ ├── SubscribeTriggerCommand.cs │ │ │ │ ├── SupportedFeaturesCommand.cs │ │ │ │ └── UnsubscribeEventsCommand.cs │ │ │ └── Messages │ │ │ │ └── HassAuthMessage.cs │ │ ├── HomeAssistantApiManager.cs │ │ ├── HomeAssistantClient.cs │ │ ├── HomeAssistantConnection.cs │ │ ├── HomeAssistantConnectionFactory.cs │ │ ├── HomeAssistantRunner.cs │ │ ├── IHomeAssistantConnectionFactory.cs │ │ ├── Json │ │ │ ├── EnsureArrayOfArrayOfStringConverter.cs │ │ │ └── EnsureStringConverter.cs │ │ └── Net │ │ │ ├── ITransportPipeline.cs │ │ │ ├── IWebSocketClient.cs │ │ │ ├── IWebSocketClientFactory.cs │ │ │ ├── IWebSocketClientTransportPipelineFactory.cs │ │ │ ├── WebSocketClientFactory.cs │ │ │ ├── WebSocketClientImpl.cs │ │ │ ├── WebSocketTransportPipeline.cs │ │ │ └── WebSocketTransportPipelineFactory.cs │ │ └── NetDaemon.Client.csproj ├── Extensions │ ├── NetDaemon.Extensions.Logging │ │ ├── Common │ │ │ └── ServiceCollectionExtensions.cs │ │ ├── Internal │ │ │ ├── LoggingConfiguration.cs │ │ │ ├── NetDaemonLoggingThemes.cs │ │ │ └── SerilogConfiguratior.cs │ │ └── NetDaemon.Extensions.Logging.csproj │ ├── NetDaemon.Extensions.MqttEntityManager │ │ ├── AssuredMqttConnection.cs │ │ ├── DependencyInjectionSetup.cs │ │ ├── Exceptions │ │ │ ├── MqttConnectionException.cs │ │ │ └── MqttPublishException.cs │ │ ├── Helpers │ │ │ ├── ByteArrayHelper.cs │ │ │ ├── EntityCreationPayloadHelper.cs │ │ │ ├── EntityIdParser.cs │ │ │ ├── IMqttFactoryWrapper.cs │ │ │ ├── JsonNodeExtensions.cs │ │ │ └── MqttFactoryWrapper.cs │ │ ├── IAssuredMqttConnection.cs │ │ ├── IMessageSender.cs │ │ ├── IMessageSubscriber.cs │ │ ├── IMqttClientOptionsFactory.cs │ │ ├── IMqttEntityManager.cs │ │ ├── IMqttFactory.cs │ │ ├── MessageSender.cs │ │ ├── MessageSubscriber.cs │ │ ├── Models │ │ │ ├── EntityCreationOptions.cs │ │ │ └── EntityCreationPayload.cs │ │ ├── MqttClientOptionsFactory.cs │ │ ├── MqttConfiguration.cs │ │ ├── MqttEntityManager.cs │ │ ├── MqttFactoryFactory.cs │ │ └── NetDaemon.Extensions.MqttEntityManager.csproj │ ├── NetDaemon.Extensions.Scheduling.Tests │ │ ├── .editorconfig │ │ ├── NetDaemon.Extensions.Scheduling.Tests.csproj │ │ └── Scheduling │ │ │ ├── CronTests.cs │ │ │ ├── DisposableSchedulerTest.cs │ │ │ ├── FakeLocalTimeZone.cs │ │ │ └── SchedulingTests.cs │ ├── NetDaemon.Extensions.Scheduling │ │ ├── CronExtensions.cs │ │ ├── DependencyInjectionSetup.cs │ │ ├── DisposableScheduler.cs │ │ ├── INetDaemonScheduler.cs │ │ ├── NetDaemon.Extensions.Scheduling.csproj │ │ ├── NetDaemonScheduler.cs │ │ └── SchedulerExtensions.cs │ └── NetDaemon.Extensions.Tts │ │ ├── GlobalUsings.cs │ │ ├── HostBuilderExtensions.cs │ │ ├── ITextToSpeechService.cs │ │ ├── Internal │ │ ├── TextToSpeechService.cs │ │ └── TtsMessage.cs │ │ └── NetDaemon.Extensions.Tts.csproj ├── HassModel │ ├── NetDaemon.HassModel.CodeGenerator │ │ ├── .editorconfig │ │ ├── CodeGeneration │ │ │ ├── AttributeTypeGenerator.cs │ │ │ ├── EntitiesGenerator.cs │ │ │ ├── EntityFactoryGenerator.cs │ │ │ ├── EnumerateAllGenerator.cs │ │ │ ├── ExtensionMethodsGenerator.cs │ │ │ ├── Generator.cs │ │ │ ├── HelpersGenerator.cs │ │ │ ├── ServiceArguments.cs │ │ │ ├── ServicesGenerator.cs │ │ │ └── SyntaxFactoryHelper.cs │ │ ├── CodeGenerationSettings.cs │ │ ├── Controller.cs │ │ ├── DependencyValidator.cs │ │ ├── Extensions │ │ │ ├── CodeGeneratorExtensions.cs │ │ │ ├── StringExtensions.cs │ │ │ └── TypeExtensions.cs │ │ ├── GlobalUsings.cs │ │ ├── HaRepositry.cs │ │ ├── Helpers │ │ │ ├── EntityIdHelper.cs │ │ │ ├── NamingHelper.cs │ │ │ └── VersionHelper.cs │ │ ├── MetaData │ │ │ ├── DefaultMetadata │ │ │ │ └── DefaultEntityMetaData.json │ │ │ ├── EntityMetaData │ │ │ │ ├── AttributeMetaDataGenerator.cs │ │ │ │ ├── ClrTypeJsonConverter.cs │ │ │ │ ├── EntityDomainMetadata.cs │ │ │ │ ├── EntityMetaDataGenerator.cs │ │ │ │ ├── EntityMetaDataMerger.cs │ │ │ │ └── NullableBoolJsonConverter.cs │ │ │ └── ServicesMetaData │ │ │ │ ├── HassService.cs │ │ │ │ ├── HassServiceArgumentMapper.cs │ │ │ │ ├── HassServiceDomain.cs │ │ │ │ ├── HassServiceField.cs │ │ │ │ ├── Response.cs │ │ │ │ ├── SelectorConverter.cs │ │ │ │ ├── Selectors.cs │ │ │ │ ├── ServiceMetaDataParser.cs │ │ │ │ ├── SingleObjectAsArrayConverter.cs │ │ │ │ ├── SnakeCaseNamingPolicy.cs │ │ │ │ ├── StringAsArrayConverter.cs │ │ │ │ └── StringAsDoubleConverter.cs │ │ ├── NetDaemon.HassModel.CodeGenerator.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── _appsettings.Development.json │ │ └── appsettings.json │ ├── NetDaemon.HassModel.Integration │ │ ├── IntegrationHaContextExtensions.cs │ │ └── NetDaemon.HassModel.Integration.csproj │ ├── NetDaemon.HassModel.Tests │ │ ├── .editorconfig │ │ ├── CodeGenerator │ │ │ ├── AttributeMetaDataGeneratorTest.cs │ │ │ ├── CodeGenTestHelper.cs │ │ │ ├── CodeGeneratorTest.cs │ │ │ ├── ControllerTest.cs │ │ │ ├── DependencyValidatorTests.cs │ │ │ ├── EntityFactoryGeneratorTest.cs │ │ │ ├── MetaDataMergerTest.cs │ │ │ ├── NetDaemonTestAppAttribute.cs │ │ │ ├── ServiceMetaDataParserTest.cs │ │ │ ├── ServiceMetaDataSamples │ │ │ │ ├── calendar.json │ │ │ │ └── light.json │ │ │ ├── ServicesGeneratorTest.cs │ │ │ └── TestFiles │ │ │ │ ├── RawVersions │ │ │ │ └── RawVersions.csproj │ │ │ │ └── SubstitutedVersions │ │ │ │ └── SubstitutedVersions.csproj │ │ ├── Entities │ │ │ ├── EntityExtensions.CallServiceWithResponseAsync.Test.cs │ │ │ ├── EntityExtensions.OnOff.Test.cs │ │ │ ├── EntityExtensions.WithCurrent.ConcreteEntity.Test.cs │ │ │ ├── EntityExtensions.WithCurrent.Entity.Test.cs │ │ │ ├── EntityTest.cs │ │ │ ├── EnumerableEntityExtensionsTest.cs │ │ │ └── NumericEntityTest.cs │ │ ├── GlobalUsings.cs │ │ ├── Integration │ │ │ └── IntegrationHaContextExtensionsTests.cs │ │ ├── Internal │ │ │ ├── AppScopedHaContextProviderTest.cs │ │ │ ├── BackgroundTaskTrackerTests.cs │ │ │ ├── EntityAreaCachTests.cs │ │ │ ├── EntityStateCacheTest.cs │ │ │ ├── HassObjectMapperTest.cs │ │ │ ├── NetDaemonExtensionsTest.cs │ │ │ ├── QueuedObservabeTest.cs │ │ │ ├── ScopedObservableTests.cs │ │ │ └── TriggerManagerTest.cs │ │ ├── NetDaemon.HassModel.Tests.csproj │ │ ├── ObservableExtensionsTest.cs │ │ ├── Registry │ │ │ └── RegistryNavigationTest.cs │ │ ├── ServiceTargetTest.cs │ │ ├── StateObservableExtensionsTest.cs │ │ └── TestHelpers │ │ │ ├── Extensions.cs │ │ │ └── HassClient │ │ │ ├── HaContextMock.cs │ │ │ └── TestEntity.cs │ └── NetDaemon.HassModel │ │ ├── Context.cs │ │ ├── DependencyInjectionSetup.cs │ │ ├── Entities │ │ ├── Core │ │ │ ├── CoreInterfaces.cs │ │ │ ├── IEntityCore.cs │ │ │ ├── LightAttributesBase.cs │ │ │ └── MediaPlayerAttributesBase.cs │ │ ├── DefaultEntityFactory.cs │ │ ├── Entity.cs │ │ ├── EntityExtensions.cs │ │ ├── EntityState.cs │ │ ├── EnumerableEntityExtensions.cs │ │ ├── NumericEntity.cs │ │ ├── NumericEntityState.cs │ │ └── StateChange.cs │ │ ├── Event.cs │ │ ├── GlobalUsings.cs │ │ ├── HaContextExtensions.cs │ │ ├── HaRegistry.cs │ │ ├── ICacheManager.cs │ │ ├── IEntityFactory.cs │ │ ├── IHaContext.cs │ │ ├── IHaRegistry.cs │ │ ├── IHaRegistryNavigator.cs │ │ ├── ITriggerManager.cs │ │ ├── Internal │ │ ├── AppScopedHaContextProvider.cs │ │ ├── BackgroundTaskTracker.cs │ │ ├── CacheManager.cs │ │ ├── EntityStateCache.cs │ │ ├── ExtensionMethods.cs │ │ ├── FormatHelpers.cs │ │ ├── HassObjectMapper.cs │ │ ├── IBackgroundTaskTracker.cs │ │ ├── QueuedObservable.cs │ │ ├── RegistryCache.cs │ │ ├── ScopedObservable.cs │ │ └── TriggerManager.cs │ │ ├── NetDaemon.HassModel.csproj │ │ ├── ObservableExtensions.cs │ │ ├── Registry │ │ ├── Area.cs │ │ ├── ConversationOptions.cs │ │ ├── Device.cs │ │ ├── EntityOptions.cs │ │ ├── EntityRegistration.cs │ │ ├── Floor.cs │ │ └── Label.cs │ │ ├── ServiceTarget.cs │ │ ├── StateObservableExtensions.cs │ │ └── TriggerManagerExtensions.cs ├── Host │ ├── .gitignore │ └── NetDaemon.Host.Default │ │ ├── NetDaemon.Host.Default.csproj │ │ ├── Program.cs │ │ ├── _appsettings.Development.json │ │ └── appsettings.json ├── Runtime │ ├── NetDaemon.Runtime.Tests │ │ ├── .editorconfig │ │ ├── Fixtures │ │ │ └── LocalApp.cs │ │ ├── GlobalUsings.cs │ │ ├── Helpers │ │ │ ├── HomeAssistantRunnerMock.cs │ │ │ └── TestSettings.cs │ │ ├── Integration │ │ │ └── TestRuntime.cs │ │ ├── Internal │ │ │ ├── AppStateManagerTests.cs │ │ │ ├── AppStateRepositoryTests.cs │ │ │ ├── EntityMapperHelperTests.cs │ │ │ └── NetDaemonRuntimeTests.cs │ │ ├── NetDaemon.Runtime.Tests.csproj │ │ └── appsettings.json │ └── NetDaemon.Runtime │ │ ├── Common │ │ ├── Extensions │ │ │ ├── HostBuilderExtensions.cs │ │ │ ├── ServiceBuilderExtensions.cs │ │ │ └── ServiceCollectionExtensions.cs │ │ ├── INetDaemonRuntime.cs │ │ └── IRuntime.cs │ │ ├── GlobalUsings.cs │ │ ├── Internal │ │ ├── AppStateManager.cs │ │ ├── AppStateRepository.cs │ │ ├── EntityMapperHelper.cs │ │ ├── IAppStateRepository.cs │ │ ├── IHandleHomeAssistantAppStateUpdates.cs │ │ ├── NetDaemonRuntime.cs │ │ ├── RuntimeService.cs │ │ └── file.json │ │ └── NetDaemon.Runtime.csproj ├── Targets │ └── Sourcelink.targets └── debug │ ├── DebugHost │ ├── DebugHost.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── _appsettings.Development.json │ ├── apps │ │ ├── Client │ │ │ ├── .editorconfig │ │ │ ├── ClientDebug.cs │ │ │ ├── HelpersApp.cs │ │ │ └── LabelDebug.cs │ │ ├── ConcurrencyTestApp.cs │ │ ├── Config │ │ │ ├── Config.yaml │ │ │ └── ConfigApp.cs │ │ ├── Extensions │ │ │ ├── MqttEntityManagerApp.cs │ │ │ └── MqttEntitySubscriptionApp.cs │ │ ├── HaRegistry │ │ │ └── RegistryApp.cs │ │ ├── HelloApp │ │ │ └── HelloApp.cs │ │ ├── ServiceCall │ │ │ └── ServiceApp.cs │ │ └── YamlApp │ │ │ ├── YamlApp.cs │ │ │ └── YamlApp.yaml │ ├── apps_src │ │ └── hellow.cs │ └── appsettings.json │ ├── DebugWebHost │ ├── DebugWebHost.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── _appsettings.Development.json │ └── appsettings.json │ ├── MyLibrary │ ├── InternalHomeAssistantGenerated │ │ └── LightEntityExtensionMethods.cs │ ├── MyLibrary.csproj │ └── MyLibraryClass.cs │ └── MyNDApp │ ├── .gitignore │ ├── HomeAssistantGenerated.cs │ ├── MyNDApp.csproj │ ├── apps │ └── HassModel │ │ └── MyInterfaceAutomation │ │ └── InterfaceUsage.cs │ └── program.cs └── tests └── Integration ├── HA ├── config │ ├── automations.yaml │ ├── configuration.yaml │ ├── scenes.yaml │ ├── scripts.yaml │ └── secrets.yaml └── docker-compose.yaml ├── NetDaemon.Tests.Integration ├── .editorconfig ├── BasicTest.cs ├── CalendarTests.cs ├── CodegenIntegrationTests.cs ├── HA │ ├── config │ │ ├── automations.yaml │ │ ├── configuration.yaml │ │ ├── groups.yaml │ │ ├── scenes.yaml │ │ ├── scripts.yaml │ │ └── secrets.yaml │ └── docker-compose.yaml ├── Helpers │ ├── HomeAssistantCollection.cs │ ├── HomeAssistantLifetime.cs │ ├── HomeAssistantTestContainer │ │ ├── HomeAssistantConfiguration.cs │ │ ├── HomeAssistantContainer.cs │ │ └── HomeAssistantContainerBuilder.cs │ └── NetDaemonIntegrationBase.cs ├── HelpersTest.cs ├── NetDaemon.Tests.Integration.csproj ├── _appsettings.Development.json ├── apps │ └── Config.yaml └── appsettings.json └── README.md /.devcontainer/.bashrc: -------------------------------------------------------------------------------- 1 | GOPATH=$HOME/go 2 | function _update_ps1() { 3 | PS1="$($GOPATH/bin/powerline-go -error $?)" 4 | } 5 | if [ "$TERM" != "linux" ] && [ -f "$GOPATH/bin/powerline-go" ]; then 6 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" 7 | fi -------------------------------------------------------------------------------- /.devcontainer/install_prettyprompt.sh: -------------------------------------------------------------------------------- 1 | cp /workspaces/netdaemon/.devcontainer/.bashrc ~/ 2 | chmod 755 ~/.bashrc -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [helto4real] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/ij1qXRM6E'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: New general issue 4 | url: https://github.com/net-daemon/netdaemon/issues/new 5 | about: This is the main issue tracker for NetDaemon. Please report issues with the NetDaemon there. 6 | - name: Report incorrect or missing information on the docs 7 | url: https://github.com/net-daemon/docs/issues 8 | about: Our documentation has its own issue tracker. Please report issues with the website there or even better contribute a PR. 9 | - name: I have a question or need support 10 | url: https://discord.gg/K3xwfcX 11 | about: Joint the Discord server to get support from the community. 12 | - name: Add and discuss a feature request 13 | url: https://github.com/net-daemon/netdaemon/discussions 14 | about: Please join the github discussions and ask for a feature in the feature request channel 15 | - name: I'm unsure where to go 16 | url: https://discord.gg/K3xwfcX 17 | about: If you are unsure where to go, then joining our chat in Discord is recommended; Just ask! 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | ## The problem 15 | 16 | 22 | ## The proposed solution 23 | 24 | 28 | ## The alternatives 29 | 30 | 33 | ## Additional context 34 | 35 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | todo: 2 | keyword: "// todo:" 3 | body: "// body:" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - dependencies 10 | - "pr: dependency-update" 11 | ignore: 12 | - dependency-name: YamlDotNet 13 | versions: 14 | - 11.1.0 15 | 16 | - package-ecosystem: github-actions 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | labels: 21 | - dependencies 22 | - "pr: dependency-update" 23 | -------------------------------------------------------------------------------- /.github/matchers/dotnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "dotnet-file", 5 | "pattern": [ 6 | { 7 | "regexp": "^(.+)\\((\\d+).+\\):\\s(\\w+)\\s(.+)\\:\\s(.*)$", 8 | "file": 1, 9 | "line": 2, 10 | "severity": 3, 11 | "code": 4, 12 | "message": 5 13 | } 14 | ] 15 | }, 16 | { 17 | "owner": "dotnet-run", 18 | "pattern": [ 19 | { 20 | "regexp": "^.+\\s\\:\\s(\\w+)\\s(.*)\\:\\s(.*)$", 21 | "severity": 1, 22 | "code": 2, 23 | "message": 3 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'Release $RESOLVED_VERSION' 2 | tag-template: '$RESOLVED_VERSION' 3 | change-template: '- #$NUMBER $TITLE @$AUTHOR' 4 | sort-direction: ascending 5 | categories: 6 | - title: ':boom: Breaking changes' 7 | label: 'pr: breaking change' 8 | 9 | - title: ':sparkles: New features' 10 | label: 'pr: new-feature' 11 | 12 | - title: ':zap: Enhancements' 13 | label: 'pr: enhancement' 14 | 15 | - title: ':bug: Bug Fixes' 16 | label: 'pr: bugfix' 17 | 18 | - title: ':arrow_up: Dependency Updates' 19 | labels: 20 | - 'pr: dependency-update' 21 | - 'dependencies' 22 | 23 | include-labels: 24 | - 'pr: breaking change' 25 | - 'pr: enhancement' 26 | - 'pr: dependency-update' 27 | - 'pr: new-feature' 28 | - 'pr: bugfix' 29 | 30 | template: | 31 | ## 👀 Summary 32 | 33 | $CHANGES 34 | 35 | ## Links 36 | - [Discord server for NetDaemon](https://discord.gg/GFzBYpuCG3) 37 | - [NetDaemon Documentation](https://netdaemon.xyz) 38 | - [If you like what I (@helto4real) do please consider sponsoring me on GitHub](https://github.com/sponsors/helto4real) 39 | - [Or buy me a ☕️ / 🍺](https://www.buymeacoffee.com/ij1qXRM6E) -------------------------------------------------------------------------------- /.github/workflows/check_pr_label.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Check labels 2 | on: 3 | pull_request: 4 | types: [opened, labeled, unlabeled, synchronize, reopened] 5 | jobs: 6 | check_labels: 7 | name: 🏷️ Check valid labels 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check valid labels 11 | run: | 12 | labels=$(jq -r '.pull_request.labels[] | .name' ${{github.event_path }} | grep 'pr:') 13 | if [[ ! $labels ]]; then 14 | echo "::error::You need to provide one or more labels that starts with 'pr:'" 15 | exit 1 16 | fi 17 | exit 0 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/ci_analyze.yml: -------------------------------------------------------------------------------- 1 | ### Build and tests all pushes, also code coverage 2 | name: 🔍 CI Analyze sources 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | sonarscanner: 10 | name: 🔍 SonarScanner 11 | environment: CI - analyze environment 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 📤 Checkout the repository 15 | uses: actions/checkout@main 16 | with: 17 | # Shallow clones should be disabled for a better relevancy of analysis 18 | fetch-depth: 0 19 | 20 | - name: 🔍 Analyze code 21 | uses: highbyte/sonarscan-dotnet@v2.4.2 22 | env: 23 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | sonarOrganization: net-daemon 27 | sonarProjectKey: net-daemon_netdaemon 28 | sonarProjectName: netdaemon 29 | dotnetTestArguments: --logger trx /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 30 | sonarBeginArguments: >- 31 | /d:sonar.inclusions="**/src/**" 32 | /d:sonar.test.inclusions="**/tests/**" 33 | /d:sonar.cs.xunit.reportsPaths="**/tests/**/TestResults/*.trx" 34 | /d:sonar.cs.opencover.reportsPaths="**/tests/**/coverage.opencover.xml" 35 | -------------------------------------------------------------------------------- /.github/workflows/common/set_netdaemon_version.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | version: 5 | required: true 6 | type: string 7 | jobs: 8 | set_version_in_source: 9 | name: 📆 Set version in code and docker files 10 | runs-on: ubuntu-latest 11 | steps: 12 | run: | 13 | echo setting source version: ${{ inputs.version }} 14 | sed -i '/ private const string Version = /c\ private const string Version = "${{ inputs.version }}";' ${{github.workspace}}/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs 15 | sed -i '/ io.hass.version=/c\ io.hass.version="${{ inputs.version }}"' ${{github.workspace}}/Dockerfile.AddOn -------------------------------------------------------------------------------- /.github/workflows/release_drafter.yml: -------------------------------------------------------------------------------- 1 | name: 📝 Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | update: 9 | name: ⏫ Update 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 📥 Checkout the repository 13 | uses: actions/checkout@main 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: ⏭️ Get next version 18 | id: version 19 | run: | 20 | declare -i newpost 21 | latest=$(git describe --tags $(git rev-list --tags --max-count=1)) 22 | latestpre=$(echo "$latest" | awk '{split($0,a,"."); print a[1] "." a[2]}') 23 | datepre=$(date --utc '+%y.%-W') 24 | if [[ "$latestpre" == "$datepre" ]]; then 25 | latestpost=$(echo "$latest" | awk '{split($0,a,"."); print a[3]}') 26 | newpost=$latestpost+1 27 | else 28 | newpost=0 29 | fi 30 | echo Current version: $latest 31 | echo New target version: $datepre.$newpost 32 | echo "version=$datepre.$newpost" >> $GITHUB_OUTPUT 33 | 34 | - name: 🏃 Run Release Drafter 35 | uses: release-drafter/release-drafter@v6 36 | with: 37 | tag: ${{ steps.version.outputs.version }} 38 | name: ${{ steps.version.outputs.version }} 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/test_docker.yml: -------------------------------------------------------------------------------- 1 | #### Publish tags to docker hub 2 | name: 👀 Test docker 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - dev 8 | 9 | jobs: 10 | test: 11 | name: 👀 Test image build ${{ matrix.dockerfile }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | dockerfile: 16 | - Dockerfile 17 | - Dockerfile.AddOn 18 | steps: 19 | - name: 📤 Checkout the repository 20 | uses: actions/checkout@main 21 | 22 | - name: 📎 Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: 🔧 Set up Docker Buildx 26 | id: buildx 27 | uses: docker/setup-buildx-action@v3 28 | 29 | - name: 🧰 Available platforms 30 | run: echo ${{ steps.buildx.outputs.platforms }} 31 | 32 | - name: 🛠️ Run Buildx 33 | run: | 34 | docker buildx build \ 35 | --platform linux/arm,linux/arm64,linux/amd64 \ 36 | --output "type=image,push=false" \ 37 | --no-cache \ 38 | --file ./${{ matrix.dockerfile }} . \ 39 | --tag netdaemon/netdaemon5:dev 40 | -------------------------------------------------------------------------------- /.linting/roslynator.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug OldModel", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "justMyCode": false, 13 | // If you have changed target frameworks, make sure to update the program path. 14 | "program": "${workspaceFolder}/dev/DebugHost/bin/Debug/net6.0/DebugHost.dll", 15 | "args": [], 16 | "cwd": "${workspaceFolder}/dev/DebugHost", 17 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 18 | "console": "internalConsole", 19 | "stopAtEntry": false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.csproj.user": true, 4 | "**/bin": true, 5 | "**/codecover": true, 6 | "**/documentation/site": true, 7 | "**/obj": true, 8 | "**/Properties": true, 9 | "**/TestResults": true 10 | }, 11 | "omnisharp.enableRoslynAnalyzers": true, 12 | "cSpell.words": [ 13 | "Expando", 14 | "Finalizers", 15 | "Hass", 16 | "Usings", 17 | "Xunit", 18 | "dayofweek", 19 | "entityid", 20 | "hassio", 21 | "idguid", 22 | "mulitinstance", 23 | "mydomain", 24 | "myevent", 25 | "mylight", 26 | "myscript", 27 | "noexist", 28 | "noquotes", 29 | "parentidguid", 30 | "useridguid" 31 | ], 32 | "dotnet.defaultSolution": "NetDaemon.sln" 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/NetDaemon.sln", 11 | "/property:GenerateFullPaths=true" 12 | ], 13 | "problemMatcher": "$msCompile" 14 | }, 15 | { 16 | "label": "run integration tests", 17 | "command": "dotnet", 18 | "type": "process", 19 | "args": [ 20 | "run ", 21 | "${workspaceFolder}/NetDaemon.sln", 22 | "/property:GenerateFullPaths=true" 23 | ], 24 | "problemMatcher": "$msCompile" 25 | }, 26 | { 27 | "label": "test coverage", 28 | "command": "dotnet", 29 | "type": "process", 30 | "group": { 31 | "kind": "test", 32 | "isDefault": true 33 | }, 34 | "args": [ 35 | "test", 36 | "${workspaceFolder}/NetDaemon.sln", 37 | "/p:CollectCoverage=true", 38 | "/p:CoverletOutputFormat=lcov", 39 | "/p:CoverletOutput=../codecover/lcov.info" 40 | ], 41 | "problemMatcher": "$msCompile" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NetDaemon 2 | 3 | Everyone is welcome to contribute to NetDaemon. If you are not a developer you might help out with the documentation at [netdaemon.xyz](netdaemon.xyz). 4 | You will find the docs on [github here](https://github.com/net-daemon/docs). 5 | 6 | ## How to contribute 7 | 8 | - Fork the repository 9 | - Write code and tests 10 | - Make sure the tests run before PR 11 | - Create a pullrequest against the NetDaemon main branch 12 | 13 | ## Code quality 14 | 15 | - Use default code quality checks and fix all warnings before PR review as warnings will fail the build process. 16 | 17 | ## Architecture 18 | 19 | - If you want to do bigger design changes, use the Discord group and discuss in #dev channel before putting too much work into it, see https://discord.gg/K3xwfcX 20 | 21 | ## Features and Issues 22 | 23 | Check if there are features to implement or file issued at [https://github.com/net-daemon/netdaemon/issues](https://github.com/net-daemon/netdaemon/issues) 24 | 25 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | # Developing NetDaemon 2 | 3 | Thank you for considering contributing to NetDaemon. You will find a good starting point in the [documentation for developers](https://netdaemon.xyz/docs/developer) 4 | 5 | Good luck 6 | -------------------------------------------------------------------------------- /Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/net-daemon/netdaemon/bde37bf45bf573decb4c8c293baf9a49aef5bb68/Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon -------------------------------------------------------------------------------- /Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon_addon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/net-daemon/netdaemon/bde37bf45bf573decb4c8c293baf9a49aef5bb68/Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon_addon -------------------------------------------------------------------------------- /Docker/rootfs/etc/services.d/netdaemon/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /Docker/rootfs/etc/services.d/netdaemon/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /Docker/rootfs/etc/services.d/netdaemon_addon/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /Docker/rootfs/etc/services.d/netdaemon_addon/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /Docker/s6.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://github.com/just-containers/s6-overlay 3 | 4 | S6_VERSION="v2.1.0.2" 5 | ARCH=$(uname -m) 6 | 7 | if [[ "${ARCH}" == "armv7l" ]]; then 8 | wget -q -nv -O /tmp/s6.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/${S6_VERSION}/s6-overlay-armhf.tar.gz" 9 | elif [[ "${ARCH}" == "aarch64" ]]; then 10 | wget -q -nv -O /tmp/s6.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/${S6_VERSION}/s6-overlay-aarch64.tar.gz" 11 | elif [[ "${ARCH}" == "x86_64" ]]; then 12 | wget -q -nv -O /tmp/s6.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/${S6_VERSION}/s6-overlay-amd64.tar.gz" 13 | else 14 | echo "NOT SUPPORTED ARCH: ${ARCH}" 15 | exit 1 16 | fi 17 | 18 | tar xzf /tmp/s6.tar.gz -C / 19 | rm /tmp/s6.tar.gz 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Pre-build .NET NetDaemon core project 2 | FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim-amd64 as netbuilder 3 | ARG TARGETPLATFORM 4 | ARG BUILDPLATFORM 5 | 6 | RUN echo "I am running on ${BUILDPLATFORM}" 7 | RUN echo "building for ${TARGETPLATFORM}" 8 | RUN export TARGETPLATFORM="${TARGETPLATFORM}" 9 | 10 | # Copy the source to docker container 11 | COPY . /usr 12 | 13 | RUN dotnet publish /usr/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj -o "/daemon" 14 | 15 | # Final stage, create the runtime container 16 | FROM ghcr.io/net-daemon/netdaemon_base:9 17 | 18 | # # Install S6 and the Admin site 19 | # COPY ./Docker/rootfs/etc/services.d/NetDaemonAdmin /etc/services.d/NetDaemonAdmin 20 | COPY --chmod=755 ./Docker/rootfs/etc/services.d/netdaemon /etc/s6-overlay/s6-rc.d/netdaemon 21 | COPY --chmod=755 ./Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon etc/s6-overlay/s6-rc.d/user/contents.d/netdaemon 22 | # COPY admin 23 | # COPY --from=builder /admin /admin 24 | COPY --from=netbuilder /daemon /daemon 25 | 26 | # This is always set to data as default 27 | ENV NetDaemon__ApplicationConfigurationFolder=/data 28 | -------------------------------------------------------------------------------- /Dockerfile.AddOn: -------------------------------------------------------------------------------- 1 | # No admin support yet, we need to build the websocket API 2 | # Pre-build .NET NetDaemon core project 3 | FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim-amd64 as netbuilder 4 | ARG TARGETPLATFORM 5 | ARG BUILDPLATFORM 6 | 7 | RUN echo "I am running on ${BUILDPLATFORM}" 8 | RUN echo "building for ${TARGETPLATFORM}" 9 | 10 | RUN export TARGETPLATFORM="${TARGETPLATFORM}" 11 | 12 | # Copy the source to docker container 13 | COPY . /usr 14 | RUN dotnet publish /usr/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj -o "/daemon" 15 | 16 | # Final stage, create the runtime container 17 | FROM ghcr.io/net-daemon/netdaemon_addonbase:9 18 | 19 | # # Install S6 and the Admin site 20 | COPY --chmod=755 ./Docker/rootfs/etc/services.d/netdaemon_addon /etc/s6-overlay/s6-rc.d/netdaemon_addon 21 | COPY --chmod=755 ./Docker/rootfs/etc/s6-overlay/s6-rc.d/usr/netdaemon_addon etc/s6-overlay/s6-rc.d/user/contents.d/netdaemon_addon 22 | # COPY admin 23 | # COPY --from=builder /admin /admin 24 | COPY --from=netbuilder /daemon /daemon 25 | 26 | LABEL \ 27 | io.hass.version="VERSION" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tomas Hellström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 0 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: 10 | tests: # declare a new status context "tests" 11 | target: 70% # we always want 70% coverage here 12 | patch: 13 | default: 14 | # basic 15 | target: auto 16 | threshold: 10% 17 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.0", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/net-daemon/netdaemon/bde37bf45bf573decb4c8c293baf9a49aef5bb68/img/icon.png -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/Compiler/CollectableAssemblyLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.Loader; 3 | 4 | namespace NetDaemon.AppModel.Internal.Compiler; 5 | 6 | internal class CollectableAssemblyLoadContext : AssemblyLoadContext 7 | { 8 | public CollectableAssemblyLoadContext() : base(true) 9 | { 10 | } 11 | 12 | protected override Assembly? Load(AssemblyName assemblyName) 13 | { 14 | return null; 15 | } 16 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/Compiler/CompileSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Internal.Compiler; 2 | 3 | /// 4 | /// Used to set debug or release mode for dynamic compiled apps 5 | /// 6 | internal record CompileSettings 7 | { 8 | public bool UseDebug { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/Compiler/ICompiler.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Internal.Compiler; 2 | 3 | internal interface ICompiler : IDisposable 4 | { 5 | CompiledAssemblyResult Compile(); 6 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/Compiler/ISyntaxTreeResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace NetDaemon.AppModel.Internal.Compiler; 4 | 5 | /// 6 | /// Gets the syntax tree from any source code (file, string) 7 | /// 8 | internal interface ISyntaxTreeResolver 9 | { 10 | IReadOnlyCollection GetSyntaxTrees(); 11 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/DynamicallyCompiledAppAssemblyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using NetDaemon.AppModel.Internal.Compiler; 3 | 4 | namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; 5 | 6 | internal class DynamicallyCompiledAppAssemblyProvider : IAppAssemblyProvider, IDisposable 7 | { 8 | private readonly ICompiler _compiler; 9 | 10 | private Assembly? _compiledAssembly; 11 | private CollectableAssemblyLoadContext? _currentContext; 12 | 13 | public DynamicallyCompiledAppAssemblyProvider(ICompiler compiler) 14 | { 15 | _compiler = compiler; 16 | } 17 | 18 | public Assembly GetAppAssembly() 19 | { 20 | // We reuse an already compiled assembly since we only compile once per start 21 | if (_compiledAssembly is not null) 22 | return _compiledAssembly; 23 | 24 | var (loadContext, compiledAssembly) = _compiler.Compile(); 25 | _currentContext = loadContext; 26 | _compiledAssembly = compiledAssembly; 27 | return compiledAssembly; 28 | } 29 | 30 | public void Dispose() 31 | { 32 | if (_currentContext is null) return; 33 | _currentContext.Unload(); 34 | // Finally do cleanup and release memory 35 | GC.Collect(); 36 | GC.WaitForPendingFinalizers(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.SourceDeployedApps/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Runtime.CompilerServices; 4 | global using System.Text; 5 | 6 | // Make the internal visible to test project 7 | [assembly: InternalsVisibleTo("NetDaemon.AppModel.Tests")] 8 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 9 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.xUnit1030.severity = none -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Compiler/Fixtures/SimpleApp.cs: -------------------------------------------------------------------------------- 1 | namespace test; 2 | 3 | public class SimpleApp 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/FailedConfig/Fail.yaml: -------------------------------------------------------------------------------- 1 | DuplicateKeySetting: 2 | ThisIsDuplicated: Hello test! 3 | ThisIsDuplicated: Another string -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/Fixtures/App.json: -------------------------------------------------------------------------------- 1 | { 2 | "TestConfig": { 3 | "AString": "Hello test!" 4 | } 5 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/Fixtures/App.yaml: -------------------------------------------------------------------------------- 1 | TestConfig: 2 | AString: Hello test! 3 | 4 | TestConfigConverter: 5 | Entity: light.testlight 6 | AnArray: 7 | - Hello 8 | - World 9 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/Fixtures/AppInjectSettings.yaml: -------------------------------------------------------------------------------- 1 | NetDaemon.AppModel.Tests.Config.TestSettings: 2 | AString: Hello test yaml! -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/Fixtures/AppInjectSettingsy.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetDaemon.AppModel.Tests.Config.TestSettings": { 3 | "AString": "Hello test!" 4 | } 5 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Config/Fixtures/CollectionTestsFixture.yaml: -------------------------------------------------------------------------------- 1 | TestCollections: 2 | SomeEnumerable: 3 | - A string 4 | - Another string 5 | SomeList: 6 | - A list item 7 | - Another list item 8 | SomeReadOnlyList: 9 | - Readonly list item 10 | - Another readonly list item 11 | SomeReadOnlyCollection: 12 | - Readonly collection item 13 | - Another readonly collection item 14 | SomeCollection: 15 | - Collection item 16 | - Another collection item 17 | SomeReadOnlyDictionary: 18 | ReadOnlyKey1: Item1 19 | ReadOnlyKey2: Item2 20 | SomeDictionary: 21 | ReadOnlyKey1: Item1 22 | ReadOnlyKey2: Item2 23 | 24 | AnotherTestCollections: 25 | - Plain list 26 | - Should work 27 | - too 28 | 29 | AnotherTestDictionary: 30 | key1: value1 31 | key2: value2 32 | key3: value3 33 | 34 | AnotherTestDictionaryOfLists: 35 | key1: 36 | - Item 1 37 | - Item 2 38 | key2: 39 | - Item 1 40 | - Item 2 41 | 42 | AnotherTestListOfDictionary: 43 | - Key 1: Item 1 44 | Key 2: Item 2 45 | - Key 1: Item 1 46 | Key 2: Item 2 47 | 48 | AnotherTestArray: 49 | - Item 1 50 | - Item 2 51 | 52 | AnotherTestEnum: 53 | - Enum1 54 | - Enum2 55 | 56 | AnotherTestShortEnum: 57 | - Enum1 58 | - Enum2 59 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationContextTests.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel.Internal; 2 | using NetDaemon.AppModel.Internal.AppFactories; 3 | 4 | namespace NetDaemon.AppModel.Tests.Context; 5 | 6 | public class ApplicationContextTests 7 | { 8 | [Fact] 9 | public async Task TestApplicationContextIsDisposedMultipleTimesNotThrowsException() 10 | { 11 | var serviceProvider = new ServiceCollection().BuildServiceProvider(); 12 | var appFactory = Mock.Of(); 13 | var applicationContext = new ApplicationContext(serviceProvider, appFactory); 14 | 15 | await applicationContext.DisposeAsync(); 16 | await applicationContext.DisposeAsync(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Context/ApplicationScopeTests.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel.Internal; 2 | using NetDaemon.AppModel.Internal.AppFactories; 3 | 4 | namespace NetDaemon.AppModel.Tests.Context; 5 | 6 | public class ApplicationScopeTests 7 | { 8 | /// 9 | /// This test do integration test that uses dynamic compilation. Only thing that is faked is the path to 10 | /// the apps folder that it is injected as transient to fake it. 11 | /// 12 | [Fact] 13 | public void TestFailedInitializedScopeThrows() 14 | { 15 | var scope = new ApplicationScope(); 16 | 17 | Assert.Throws(() => scope.ApplicationContext); 18 | } 19 | 20 | [Fact] 21 | public void TestInitializedScopeReturnsOk() 22 | { 23 | var scope = new ApplicationScope 24 | { 25 | ApplicationContext = new ApplicationContext( 26 | new ServiceCollection().BuildServiceProvider(), 27 | Mock.Of() 28 | ) 29 | }; 30 | var ctx = scope.ApplicationContext; 31 | 32 | ctx.Should().NotBeNull(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Dynamic/Application.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel; 2 | 3 | namespace Apps; 4 | 5 | public class TestSettings 6 | { 7 | public string AString { get; set; } = string.Empty; 8 | } 9 | 10 | [NetDaemonApp] 11 | public class MyApp 12 | { 13 | public IAppConfig Settings { get; } 14 | public MyApp(IAppConfig settings) 15 | { 16 | Settings = settings; 17 | } 18 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Dynamic/settings.yaml: -------------------------------------------------------------------------------- 1 | Apps.TestSettings: 2 | AString: Hello world! 3 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/DynamicError/IDoNotCompileWell.cs: -------------------------------------------------------------------------------- 1 | public class IDoNotCompileWell 2 | { 3 | int MissingSemiColon 4 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/DynamicWithFocus/Application.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel; 2 | 3 | namespace Apps; 4 | 5 | [NetDaemonApp] 6 | public class NonFocusApp 7 | { 8 | public NonFocusApp() 9 | { 10 | } 11 | } 12 | 13 | [NetDaemonApp] 14 | [Focus] 15 | public class MyFocusApp 16 | { 17 | public MyFocusApp() 18 | { 19 | } 20 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/DynamicWithServiceCollection/Application.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel; 2 | using Microsoft.Extensions.DependencyInjection; 3 | namespace Apps; 4 | 5 | public static class ServiceCollectionRegister 6 | { 7 | [ServiceCollectionExtension] 8 | public static void RegisterSomeGreatServices(IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | } 12 | } 13 | 14 | public class InjectMePlease 15 | { 16 | public InjectMePlease() 17 | { 18 | 19 | } 20 | 21 | public string AValue { get; init; } = "SomeInjectedValue"; 22 | } 23 | 24 | [NetDaemonApp] 25 | public class InjectedApp 26 | { 27 | public string InjectedValue { get; set; } 28 | public InjectedApp(InjectMePlease injectedClass) 29 | { 30 | InjectedValue = injectedClass.AValue; 31 | } 32 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/AppThatTakesASlowTimeToDispose.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace LocalApps; 3 | 4 | 5 | [NetDaemonApp] 6 | public sealed class SlowDisposableApp : IDisposable 7 | { 8 | public SlowDisposableApp() 9 | { 10 | } 11 | 12 | public void Dispose() 13 | { 14 | Thread.Sleep(3500); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithDisposable.cs: -------------------------------------------------------------------------------- 1 | namespace LocalApps; 2 | 3 | [NetDaemonApp] 4 | public sealed class MyAppLocalAppWithAsyncDispose : IAsyncDisposable, IDisposable 5 | { 6 | public bool AsyncDisposeIsCalled { get; private set; } 7 | public bool DisposeIsCalled { get; private set; } 8 | 9 | public ValueTask DisposeAsync() 10 | { 11 | AsyncDisposeIsCalled = true; 12 | GC.SuppressFinalize(this); 13 | return ValueTask.CompletedTask; 14 | } 15 | 16 | public void Dispose() 17 | { 18 | DisposeIsCalled = true; 19 | GC.SuppressFinalize(this); 20 | } 21 | } 22 | 23 | [NetDaemonApp] 24 | public sealed class MyAppLocalAppWithDispose : IDisposable 25 | { 26 | public bool DisposeIsCalled { get; private set; } 27 | 28 | public void Dispose() 29 | { 30 | DisposeIsCalled = true; 31 | GC.SuppressFinalize(this); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithId.cs: -------------------------------------------------------------------------------- 1 | namespace LocalApps; 2 | 3 | [NetDaemonApp(Id = Id)] 4 | public class MyAppLocalAppWithId 5 | { 6 | public const string Id = "SomeId"; 7 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/LocalAppWithInitializeAsync.cs: -------------------------------------------------------------------------------- 1 | namespace LocalApps; 2 | 3 | [NetDaemonApp] 4 | public class MyAppLocalAppWithInitializeAsync : IAsyncInitializable 5 | { 6 | public bool InitializeAsyncCalled { get; private set; } 7 | 8 | 9 | public Task InitializeAsync(CancellationToken cancellationToken) 10 | { 11 | InitializeAsyncCalled = true; 12 | return Task.CompletedTask; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/Local/settings.yaml: -------------------------------------------------------------------------------- 1 | LocalApps.LocalTestSettings: 2 | AString: Hello world! 3 | Entity: light.test 4 | Entity2: light.test2 -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Fixtures/LocalError/LocalApp.cs: -------------------------------------------------------------------------------- 1 | namespace LocalAppsWithErrors; 2 | 3 | [NetDaemonApp] 4 | public class MyAppLocalAppWithError 5 | { 6 | public MyAppLocalAppWithError() 7 | { 8 | throw new InvalidOperationException("Some error"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using Microsoft.Extensions.Options; 3 | global using Microsoft.Extensions.DependencyInjection; 4 | global using FluentAssertions; 5 | global using Moq; 6 | global using Xunit; 7 | global using NetDaemon.AppModel; 8 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Helpers/FakeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Tests.Helpers; 2 | 3 | internal sealed class FakeOptions : IOptions 4 | { 5 | public FakeOptions(string path) 6 | { 7 | Value = new AppConfigurationLocationSetting {ApplicationConfigurationFolder = path}; 8 | } 9 | 10 | public AppConfigurationLocationSetting Value { get; init; } 11 | } 12 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel.Tests/Helpers/FakeOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Tests.Helpers; 2 | 3 | internal static class FakeOptionsExtensions 4 | { 5 | public static IServiceCollection AddFakeOptions(this IServiceCollection services, string name) 6 | { 7 | return services.AddTransient(_ => CreateFakeOptions(name)); 8 | } 9 | 10 | private static IOptions CreateFakeOptions(string name) 11 | { 12 | var path = Path.Combine(AppContext.BaseDirectory, Path.Combine(AppContext.BaseDirectory, $"Fixtures/{name}")); 13 | return new FakeOptions(path); 14 | } 15 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/ApplicationState.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// The current state of an application 5 | /// 6 | public enum ApplicationState 7 | { 8 | /// 9 | /// Application is enabled 10 | /// 11 | Enabled, 12 | /// 13 | /// Application is disabled 14 | /// 15 | Disabled, 16 | /// 17 | /// The application is in running state 18 | /// 19 | Running, 20 | /// 21 | /// Application is in error state 22 | /// 23 | Error 24 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/Attributes/FocusAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Marks a class to be loaded exclusively while in development environment 5 | /// 6 | /// 7 | /// If one or more app classes have this attribute, only those apps will be loaded 8 | /// 9 | [AttributeUsage(AttributeTargets.Class)] 10 | public sealed class FocusAttribute : Attribute 11 | { 12 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/Attributes/NetDaemonAppAttribute.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | 3 | namespace NetDaemon.AppModel; 4 | 5 | /// 6 | /// Marks a class as a NetDaemonApp 7 | /// 8 | [MeansImplicitUse] 9 | [AttributeUsage(AttributeTargets.Class)] 10 | public sealed class NetDaemonAppAttribute : Attribute 11 | { 12 | /// 13 | /// Id of an app 14 | /// 15 | public string? Id { get; init; } 16 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/Attributes/ServiceCollectionExtensionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Indicates method in dynamically compile code should be called as a ServiceCollectionExtension to setup services 5 | /// 6 | /// 7 | /// The method should have `public static void RegisterServices(IServiceCollection services){}` 8 | /// 9 | [AttributeUsage(AttributeTargets.Method)] 10 | public sealed class ServiceCollectionExtensionAttribute : Attribute 11 | { 12 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IAppModel.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Application model 5 | /// 6 | public interface IAppModel 7 | { 8 | /// 9 | /// Instance and configure all applications. 10 | /// 11 | /// Cancellation token 12 | /// 13 | /// All apps that are configured with [NetDaemonApp] attribute or in yaml will be handled. 14 | /// [Focus] attribute will only load the apps that has this attribute if exist 15 | /// Depending on the selected compilation type it will be local or dynamically compiled apps 16 | /// 17 | /// 18 | Task LoadNewApplicationContext(CancellationToken cancellationToken); 19 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IAppModelContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Manage AppModel state and lifecycle 5 | /// 6 | public interface IAppModelContext : IAsyncInitializable, IAsyncDisposable 7 | { 8 | /// 9 | /// Current instantiated and running applications 10 | /// 11 | IReadOnlyCollection Applications { get; } 12 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IAppStateManager.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Let users manage state of the application with own implementation 5 | /// 6 | public interface IAppStateManager 7 | { 8 | /// 9 | /// Gets application state 10 | /// 11 | /// The unique id of the application 12 | Task GetStateAsync(string applicationId); 13 | /// 14 | /// Saves application state 15 | /// 16 | /// The unique id of the application 17 | /// The application state to save 18 | Task SaveStateAsync(string applicationId, ApplicationState state); 19 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IApplication.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Represents a application and it's state 5 | /// 6 | public interface IApplication : IAsyncDisposable 7 | { 8 | /// 9 | /// Unique id of the application 10 | /// 11 | string? Id { get; } 12 | 13 | /// 14 | /// Current state of the application 15 | /// 16 | ApplicationState State { get; } 17 | 18 | /// 19 | /// Sets state for application 20 | /// 21 | /// The state to set 22 | Task SetStateAsync(ApplicationState state); 23 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IAsyncInitializable.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Allows apps to initialize non-blocking async operations 5 | /// 6 | public interface IAsyncInitializable 7 | { 8 | /// 9 | /// Initialize async non-blocking async operations 10 | /// 11 | /// Cancellation token to that are canceled when application unloads 12 | Task InitializeAsync(CancellationToken cancellationToken); 13 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/IConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// Configuration in a app 5 | /// 6 | /// Type of class representing the config 7 | public interface IAppConfig : IOptions where T : class, new() 8 | { 9 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Common/Settings/AppLocationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel; 2 | 3 | /// 4 | /// The setting for the location of configuration files for applications 5 | /// 6 | public record AppConfigurationLocationSetting 7 | { 8 | /// 9 | /// Path to the folder where to search for application configuration files 10 | /// 11 | public string ApplicationConfigurationFolder { get; set; } = string.Empty; 12 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Runtime.CompilerServices; 4 | global using System.Threading.Tasks; 5 | global using Microsoft.Extensions.DependencyInjection; 6 | global using Microsoft.Extensions.Logging; 7 | global using Microsoft.Extensions.Options; 8 | global using NetDaemon.AppModel.Internal; 9 | 10 | // Make the internal visible to test project 11 | [assembly: InternalsVisibleTo("NetDaemon.AppModel.Tests")] 12 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 13 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/AppAssemblyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; 4 | 5 | internal class AppAssemblyProvider(Assembly assembly) : IAppAssemblyProvider 6 | { 7 | public Assembly GetAppAssembly() 8 | { 9 | return assembly; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppAssemblyProviders/IAppAssemblyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace NetDaemon.AppModel.Internal.AppAssemblyProviders; 4 | 5 | /// 6 | /// Provides interface for applications residing in assemblies 7 | /// 8 | public interface IAppAssemblyProvider 9 | { 10 | /// 11 | /// Gets the assembly that has the NetDaemon applications 12 | /// 13 | public Assembly GetAppAssembly(); 14 | } 15 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppFactories/IAppFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Internal.AppFactories; 2 | 3 | internal interface IAppFactory 4 | { 5 | object Create(IServiceProvider provider); 6 | 7 | string Id { get; } 8 | 9 | bool HasFocus { get; } 10 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/AssemblyAppFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using NetDaemon.AppModel.Internal.AppAssemblyProviders; 3 | using NetDaemon.AppModel.Internal.AppFactories; 4 | 5 | namespace NetDaemon.AppModel.Internal.AppFactoryProviders; 6 | 7 | internal class AssemblyAppFactoryProvider : IAppFactoryProvider 8 | { 9 | private readonly IEnumerable _assemblyResolvers; 10 | 11 | public AssemblyAppFactoryProvider(IEnumerable assemblyResolvers) 12 | { 13 | _assemblyResolvers = assemblyResolvers; 14 | } 15 | 16 | public IReadOnlyCollection GetAppFactories() 17 | { 18 | return _assemblyResolvers 19 | .Select(resolver => resolver.GetAppAssembly()) 20 | .SelectMany(assembly => assembly.GetTypes()) 21 | .Where(IsNetDaemonAppType) 22 | .Select(type => FuncAppFactory.Create(type)) 23 | .ToList(); 24 | } 25 | 26 | private static bool IsNetDaemonAppType(Type type) 27 | { 28 | if (!type.IsClass || type.IsGenericType || type.IsAbstract) 29 | { 30 | return false; 31 | } 32 | 33 | return type.GetCustomAttribute() is not null; 34 | } 35 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/FuncAppFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel.Internal.AppFactories; 2 | 3 | namespace NetDaemon.AppModel.Internal.AppFactoryProviders; 4 | 5 | internal class FuncAppFactoryProvider : IAppFactoryProvider 6 | { 7 | public IReadOnlyCollection GetAppFactories() 8 | { 9 | throw new NotImplementedException(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/IAppFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel.Internal.AppFactories; 2 | 3 | namespace NetDaemon.AppModel.Internal.AppFactoryProviders; 4 | 5 | internal interface IAppFactoryProvider 6 | { 7 | IReadOnlyCollection GetAppFactories(); 8 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppFactoryProviders/SingleAppFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.AppModel.Internal.AppFactories; 2 | 3 | namespace NetDaemon.AppModel.Internal.AppFactoryProviders; 4 | 5 | internal sealed class SingleAppFactoryProvider : IAppFactoryProvider 6 | { 7 | private readonly IAppFactory _factory; 8 | 9 | private SingleAppFactoryProvider(IAppFactory factory) 10 | { 11 | _factory = factory; 12 | } 13 | 14 | public IReadOnlyCollection GetAppFactories() 15 | { 16 | return new[] { _factory }; 17 | } 18 | 19 | public static IAppFactoryProvider Create(Func func, 20 | string? id = default, bool? focus = default) where TAppType : class 21 | { 22 | var factory = FuncAppFactory.Create(func, id, focus); 23 | return new SingleAppFactoryProvider(factory); 24 | } 25 | 26 | public static IAppFactoryProvider Create(Type type, string? id = default, bool? focus = default) 27 | { 28 | var factory = FuncAppFactory.Create(type, id, focus); 29 | return new SingleAppFactoryProvider(factory); 30 | } 31 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Internal; 2 | 3 | /// 4 | /// This class serves as a factory for creating and initializing new ApplicationContexts 5 | /// 6 | internal class AppModelImpl : IAppModel 7 | { 8 | private readonly IServiceProvider _provider; 9 | 10 | public AppModelImpl(IServiceProvider provider) 11 | { 12 | _provider = provider; 13 | } 14 | 15 | public async Task LoadNewApplicationContext(CancellationToken cancellationToken) 16 | { 17 | // Create a new AppModelContext 18 | var appModelContext = _provider.GetRequiredService(); 19 | await appModelContext.InitializeAsync(cancellationToken); 20 | return appModelContext; 21 | } 22 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Config/AppConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace NetDaemon.AppModel.Internal.Config; 4 | 5 | internal class AppConfig : IAppConfig where T : class, new() 6 | { 7 | public AppConfig(IConfiguration config, IConfigurationBinding configBinder, ILogger> logger) 8 | { 9 | var type = typeof(T); 10 | var section = config.GetSection(type.FullName!); 11 | 12 | if (!section.Exists()) 13 | { 14 | logger.LogWarning("The configuration for {Type} is not found. Please add config.", typeof(T).FullName); 15 | Value = new T(); 16 | } 17 | else 18 | { 19 | Value = configBinder.ToObject(section) ?? new T(); 20 | } 21 | } 22 | 23 | public T Value { get; } 24 | } 25 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Config/ConfigurationBinding.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace NetDaemon.AppModel.Internal.Config; 4 | 5 | /// 6 | /// Wrapper around the ConfigurationBinder to make it available from the service provider 7 | /// and to inject the IServiceProvider to allow instancing objects that are registered services 8 | /// 9 | internal class ConfigurationBinding(IServiceProvider provider) : IConfigurationBinding 10 | { 11 | private readonly IServiceProvider _provider = provider; 12 | 13 | public T? ToObject(IConfiguration configuration) 14 | { 15 | return configuration.Get(_provider); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Config/IConfigurationBinding.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace NetDaemon.AppModel.Internal.Config; 4 | 5 | /// 6 | /// Interface for configuration binding to object 7 | /// 8 | internal interface IConfigurationBinding 9 | { 10 | T? ToObject(IConfiguration configuration); 11 | } 12 | -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace NetDaemon.AppModel.Internal.Config; 4 | 5 | internal class YamlConfigurationProvider : FileConfigurationProvider 6 | { 7 | public YamlConfigurationProvider(YamlConfigurationSource source) : base(source) 8 | { 9 | } 10 | 11 | public override void Load(Stream stream) 12 | { 13 | var parser = new YamlConfigurationFileParser(); 14 | 15 | Data = parser.Parse(stream); 16 | } 17 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace NetDaemon.AppModel.Internal.Config; 4 | 5 | internal class YamlConfigurationSource : FileConfigurationSource 6 | { 7 | public override IConfigurationProvider Build(IConfigurationBuilder builder) 8 | { 9 | FileProvider ??= builder.GetFileProvider(); 10 | return new YamlConfigurationProvider(this); 11 | } 12 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/Internal/Context/ApplicationScope.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.AppModel.Internal; 2 | 3 | // Helper class to make ApplicationContext resolvable per scope 4 | internal class ApplicationScope 5 | { 6 | private ApplicationContext? _applicationContext; 7 | 8 | public ApplicationContext ApplicationContext 9 | { 10 | get => _applicationContext ?? 11 | throw new InvalidOperationException("ApplicationScope.ApplicationContext has not been initialized yet"); 12 | set => _applicationContext = value; 13 | } 14 | } -------------------------------------------------------------------------------- /src/AppModel/NetDaemon.AppModel/README.md: -------------------------------------------------------------------------------- 1 | # AppModel design 2 | 3 | This is the design of the AppModel. 4 | 5 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Debug/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Threading.Tasks; 2 | global using Microsoft.Extensions.DependencyInjection; 3 | global using Microsoft.Extensions.Logging; 4 | global using Microsoft.Extensions.Hosting; 5 | global using Microsoft.Extensions.Options; 6 | global using NetDaemon.Client; 7 | global using NetDaemon.Client.Extensions; 8 | global using NetDaemon.Client.HomeAssistant.Extensions; 9 | global using NetDaemon.Client.HomeAssistant.Model; 10 | global using NetDaemon.Client.Settings; -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Debug/Program.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.HassClient.Debug; 2 | 3 | await Host.CreateDefaultBuilder(args) 4 | .ConfigureServices((context, services) => 5 | { 6 | services.Configure(context.Configuration.GetSection("HomeAssistant")); 7 | services.AddHostedService(); 8 | services.AddHomeAssistantClient(); 9 | }) 10 | .Build() 11 | .RunAsync() 12 | .ConfigureAwait(false); -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Debug/_appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Warning" 6 | } 7 | }, 8 | "HomeAssistant": { 9 | "Host": "ENTER YOUR IP TO Development Home Assistant here", 10 | "Port": 8124, 11 | "Ssl": false, 12 | "Token": "ENTER YOUR TOKEN", 13 | "InsecureBypassCertificateErrors": false 14 | } 15 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Debug/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning" 6 | } 7 | }, 8 | "HomeAssistant": { 9 | "Host": "localhost", 10 | "Port": 8123, 11 | "Ssl": false, 12 | "Token": "", 13 | "InsecureBypassCertificateErrors": false 14 | } 15 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.xUnit1030.severity = none 3 | #Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly 4 | dotnet_diagnostic.CA1861.severity = none 5 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/ExtensionsTest/JsonElementExtensionTest.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.ExtensionsTest; 2 | 3 | public class JsonElementExtensionTest 4 | { 5 | [Fact] 6 | public void TestToJsonElementShouldReturnCorrectElement() 7 | { 8 | var cmd = new SimpleCommand("get_services"); 9 | 10 | var element = cmd.ToJsonElement(); 11 | 12 | element!.Value.GetProperty("type").ToString() 13 | .Should() 14 | .Be("get_services"); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/ExtensionsTest/MqttEntityManagerTests/ByteArrayHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using NetDaemon.Extensions.MqttEntityManager.Helpers; 3 | 4 | namespace NetDaemon.HassClient.Tests.ExtensionsTest.MqttEntityManagerTests; 5 | 6 | public class ByteArrayHelperTests 7 | { 8 | sealed class GoodData : IEnumerable 9 | { 10 | public IEnumerator GetEnumerator() 11 | { 12 | yield return [null!, ""]; 13 | yield return [new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f}, "hello"]; 14 | } 15 | 16 | IEnumerator IEnumerable.GetEnumerator() 17 | { 18 | return GetEnumerator(); 19 | } 20 | } 21 | 22 | [Theory] 23 | [ClassData(typeof(GoodData))] 24 | public void CanParse(byte[]? array, string expected) 25 | { 26 | ByteArrayHelper.SafeToString(array).Should().Be(expected); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/ExtensionsTest/ServiceCollectionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.Client.Extensions; 2 | 3 | namespace NetDaemon.HassClient.Tests.ExtensionsTest; 4 | 5 | public class ServiceCollectionExtensionTests 6 | { 7 | [Fact] 8 | public void TestServiceCollectionExtension() 9 | { 10 | var services = new ServiceCollection(); 11 | services.AddHomeAssistantClient(); 12 | var serviceProvider = services.BuildServiceProvider(); 13 | var hassClient = serviceProvider.GetService(); 14 | var hassRunner = serviceProvider.GetService(); 15 | var apiManager = serviceProvider.GetService(); 16 | var connection = serviceProvider.GetService(); 17 | hassClient.Should().NotBeNull(); 18 | hassRunner.Should().NotBeNull(); 19 | apiManager.Should().NotBeNull(); 20 | 21 | Assert.Null(connection); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Net.WebSockets; 3 | global using System.Reactive.Linq; 4 | global using System.Reactive.Threading.Tasks; 5 | global using System.Text; 6 | global using System.Text.Json; 7 | global using System.Text.Json.Serialization; 8 | global using System.Threading.Channels; 9 | global using Microsoft.Extensions.Logging; 10 | global using Microsoft.Extensions.Options; 11 | global using Microsoft.AspNetCore.Hosting.Server; 12 | global using Microsoft.AspNetCore.Hosting.Server.Features; 13 | global using FluentAssertions; 14 | global using Moq; 15 | global using Xunit; 16 | global using NetDaemon.Client; 17 | global using NetDaemon.Client.HomeAssistant.Model; 18 | global using NetDaemon.Client.Internal; 19 | global using NetDaemon.Client.Internal.Helpers; 20 | global using NetDaemon.Client.Internal.Net; 21 | global using NetDaemon.Client.Internal.Extensions; 22 | global using NetDaemon.Client.Internal.HomeAssistant.Commands; 23 | global using NetDaemon.Client.Settings; 24 | global using NetDaemon.Client.HomeAssistant.Extensions; 25 | global using NetDaemon.HassClient.Tests.Helpers; -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/HelperTest/HttpHandlerHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.HelperTest; 2 | 3 | public class HttpHandlerHelperTests 4 | { 5 | [Fact] 6 | public void TestHttpHandlerHelperCreateClient() 7 | { 8 | var client = HttpHelper.CreateHttpClient(); 9 | client.Should().BeOfType(); 10 | } 11 | 12 | [Fact] 13 | public void TestHttpHandlerHelperCreateHttpMessageHandler() 14 | { 15 | var client = HttpHelper.CreateHttpMessageHandler(); 16 | client.Should().BeOfType(); 17 | } 18 | 19 | [Fact] 20 | public void TestHttpHandlerHelperCreateHttpMessageHandlerIgnoreCertErrors() 21 | { 22 | // Arrange 23 | var services = new ServiceCollection(); 24 | services.AddSingleton(Options.Create(new HomeAssistantSettings 25 | {InsecureBypassCertificateErrors = true})); 26 | 27 | var provider = services.BuildServiceProvider(); 28 | 29 | // Act 30 | var client = HttpHelper.CreateHttpMessageHandler(provider); 31 | 32 | // Assert 33 | client.Should().BeOfType(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/HelperTest/VersionHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.HelperTest; 2 | 3 | public class VersionHelperTests 4 | { 5 | [Theory] 6 | [InlineData("2022.8.12", "2022.8.12")] 7 | [InlineData("2022.8.0b7", "2022.8.0")] 8 | [InlineData("2022.9.0", "2022.9.0")] 9 | [InlineData("2022.9.0b1", "2022.9.0")] 10 | public void WithoutBeta_ValidInput_ReturnsExpectedVersion(string input, string expectedOutput) 11 | { 12 | // Arrange 13 | var expectedVersion = new Version(expectedOutput); 14 | 15 | // Act 16 | var parsedVersion = VersionHelper.ReplaceBeta(input); 17 | 18 | // Assert 19 | Assert.Equal(expectedVersion, parsedVersion); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Helpers/TestSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.Helpers; 2 | 3 | public static class TestSettings 4 | { 5 | public const int DefaultTimeout = 5000; 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/HomeAssistantClientTest/HomeAssistantConnectionMock.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.HomeAssistantClientTest; 2 | 3 | internal sealed class HomeAssistantConnectionMock : Mock 4 | { 5 | private readonly Channel _responseConfigMessageChannel = Channel.CreateBounded(10); 6 | 7 | public HomeAssistantConnectionMock() 8 | { 9 | Setup(n => n.SendCommandAndReturnResponseAsync( 10 | It.IsAny(), It.IsAny())).Returns( 11 | async (SimpleCommand _, CancellationToken _) => 12 | await _responseConfigMessageChannel.Reader.ReadAsync(CancellationToken.None)); 13 | } 14 | 15 | internal void AddConfigResponseMessage(HassConfig config) 16 | { 17 | _responseConfigMessageChannel.Writer.TryWrite(config); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/HomeAssistantRunnerTest/HomeAssistantClientMock.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.HassClient.Tests.HomeAssistantClientTest; 2 | 3 | namespace NNetDaemon.HassClient.Tests.HomeAssistantRunnerTest; 4 | 5 | internal sealed class HomeAssistantClientMock : Mock 6 | { 7 | private readonly HomeAssistantConnectionMock _haConnectionMock = new(); 8 | 9 | public HomeAssistantClientMock() 10 | { 11 | // Return a mock connection as default 12 | Setup(n => 13 | n.ConnectAsync( 14 | It.IsAny(), 15 | It.IsAny(), 16 | It.IsAny(), 17 | It.IsAny(), 18 | It.IsAny(), 19 | It.IsAny() 20 | ) 21 | ).Returns( 22 | (string _, int _, bool _, string _, string _, CancellationToken _) => 23 | Task.FromResult(_haConnectionMock.Object)); 24 | 25 | _haConnectionMock.Setup(n => 26 | n.WaitForConnectionToCloseAsync(It.IsAny()) 27 | ).Returns( 28 | async (CancellationToken cancelToken) => 29 | { 30 | await Task.Delay(TestSettings.DefaultTimeout, cancelToken).ConfigureAwait(false); 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/ApiIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.Integration; 2 | 3 | public class ApiIntegrationTests : IntegrationTestBase 4 | { 5 | public ApiIntegrationTests(HomeAssistantServiceFixture fixture) : base(fixture) 6 | { 7 | } 8 | 9 | [Fact] 10 | public async Task TestGetApiCall() 11 | { 12 | await using var ctx = await GetConnectedClientContext().ConfigureAwait(false); 13 | var entity = await ctx.HomeAssistantConnection.GetApiCallAsync("devices", TokenSource.Token) 14 | .ConfigureAwait(false); 15 | 16 | Assert.NotNull(entity?.EntityId); 17 | 18 | entity!.EntityId 19 | .Should() 20 | .BeEquivalentTo("test.entity"); 21 | } 22 | 23 | [Fact] 24 | public async Task TestPostApiCall() 25 | { 26 | await using var ctx = await GetConnectedClientContext().ConfigureAwait(false); 27 | var entity = await ctx.HomeAssistantConnection.PostApiCallAsync( 28 | "devices", 29 | TokenSource.Token, 30 | new {somedata = "hello"} 31 | ).ConfigureAwait(false); 32 | 33 | Assert.NotNull(entity?.EntityId); 34 | 35 | entity!.EntityId 36 | .Should() 37 | .BeEquivalentTo("test.post"); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/HomeAssistantServiceFixture.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.Integration; 2 | 3 | public class HomeAssistantServiceFixture : IAsyncLifetime 4 | { 5 | public HomeAssistantMock? HaMock { get; set; } 6 | 7 | public async Task DisposeAsync() 8 | { 9 | if (HaMock is not null) 10 | await HaMock.DisposeAsync().ConfigureAwait(false); 11 | } 12 | 13 | public Task InitializeAsync() 14 | { 15 | HaMock = new HomeAssistantMock(); 16 | return Task.CompletedTask; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/TestContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.Integration; 2 | 3 | internal sealed record TestContext : IAsyncDisposable 4 | { 5 | public Mock> HomeAssistantLogger { get; init; } = new(); 6 | public Mock> TransportPipelineLogger { get; init; } = new(); 7 | public Mock> HomeAssistantConnectionLogger { get; init; } = new(); 8 | public IHomeAssistantConnection HomeAssistantConnection { get; init; } = new Mock().Object; 9 | 10 | public async ValueTask DisposeAsync() 11 | { 12 | await HomeAssistantConnection.DisposeAsync().ConfigureAwait(false); 13 | GC.SuppressFinalize(this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/auth_notok.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "auth_invalid", 3 | "message": "Invalid password" 4 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/auth_ok.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "auth_ok", 3 | "ha_version": "23.1.0" 4 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/auth_required.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "auth_required" 3 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/deviceregistry_update.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 36, 4 | "type": "event", 5 | "event": { 6 | "event_type": "device_registry_updated", 7 | "data": { 8 | "action": "update", 9 | "device_id": "cc3fc35a71134624839dbda54e75b194" 10 | }, 11 | "origin": "LOCAL", 12 | "time_fired": "2020-05-05T10:14:11.881770+00:00", 13 | "context": { 14 | "id": "7ca81c93474e41d59df5277de646bfc5", 15 | "parent_id": null, 16 | "user_id": null 17 | } 18 | } 19 | }, 20 | { 21 | "id": 48, 22 | "type": "event", 23 | "event": { 24 | "event_type": "area_registry_updated", 25 | "data": { 26 | "action": "create", 27 | "area_id": "ac987a9fcafd4edc8a0b73c39eba558d" 28 | }, 29 | "origin": "LOCAL", 30 | "time_fired": "2020-05-05T10:18:58.387635+00:00", 31 | "context": { 32 | "id": "6376f6154e9d486aa2c977f0c85a307e", 33 | "parent_id": null, 34 | "user_id": null 35 | } 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/pong.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "pong" 4 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_calendar_list_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": { 6 | "context": { 7 | "id": "01H5X83MG4X9ZPT436JRBH6HXQ", 8 | "parent_id": null, 9 | "user_id": "9587bf0916cf498fa289523c229ccdb4" 10 | }, 11 | "response": { 12 | "events": [ 13 | { 14 | "start": "2023-07-21T22:00:00+00:00", 15 | "end": "2023-07-21T23:00:00+00:00", 16 | "summary": "Test", 17 | "description": "A calendar event" 18 | } 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_get_areas.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": [ 6 | { 7 | "name": "Bedroom", 8 | "area_id": "5a30cdc2fd7f44d5a77f2d6f6d2ccd76" 9 | }, 10 | { 11 | "name": "Kitchen", 12 | "area_id": "42a6048dc0404595b136545f6745c5d1" 13 | }, 14 | { 15 | "name": "Livingroom", 16 | "area_id": "4e65b6fe3cea4604ab318b0f9c2b8432" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_get_devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": [ 6 | { 7 | "config_entries": [], 8 | "connections": [], 9 | "manufacturer": "Google Inc.", 10 | "model": "Chromecast", 11 | "name": "My TV", 12 | "sw_version": null, 13 | "id": "42cdda32a2a3428e86c2e27699d79ead", 14 | "via_device_id": null, 15 | "area_id": null, 16 | "name_by_user": null 17 | }, 18 | { 19 | "config_entries": [ 20 | "4b85129c61c74b27bd90e593f6b7482e" 21 | ], 22 | "connections": [], 23 | "manufacturer": "Plex", 24 | "model": "Plex Web", 25 | "name": "Plex (Plex Web - Chrome)", 26 | "sw_version": "4.22.3", 27 | "id": "49b27477238a4c8fb6cc8fbac32cebbc", 28 | "via_device_id": "6e17380a6d2744d18045fe4f627db706", 29 | "area_id": null, 30 | "name_by_user": null 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_get_entities.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": [ 6 | { 7 | "config_entry_id": null, 8 | "device_id": "42cdda32a2a3428e86c2e27699d79ead", 9 | "disabled_by": null, 10 | "entity_id": "media_player.tv_uppe2", 11 | "area_id": "42cdda1212a3428e86c2e27699d79ead", 12 | "name": null, 13 | "icon": null, 14 | "platform": "cast", 15 | "options": { 16 | "conversation": { 17 | "should_expose": true 18 | } 19 | } 20 | 21 | }, 22 | { 23 | "config_entry_id": "4b85129c61c74b27bd90e593f6b7482e", 24 | "device_id": "6e17380a6d2744d18045fe4f627db706", 25 | "disabled_by": null, 26 | "entity_id": "sensor.plex_plex", 27 | "name": null, 28 | "icon": null, 29 | "platform": "plex", 30 | "options": { 31 | "conversation": { 32 | "should_expose": false 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_get_floors.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": [ 6 | { 7 | "aliases": [], 8 | "floor_id": "floor0", 9 | "icon": null, 10 | "level": 0, 11 | "name": "Floor 0" 12 | }, 13 | { 14 | "aliases": [], 15 | "floor_id": "floor1", 16 | "icon": null, 17 | "level": 1, 18 | "name": "Floor 1" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_get_labels.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": [ 6 | { 7 | "color": "green", 8 | "description": null, 9 | "icon": "mdi:chair-rolling", 10 | "label_id": "label1", 11 | "name": "Label 1" 12 | }, 13 | { 14 | "color": "indigo", 15 | "description": null, 16 | "icon": "mdi:lightbulb-night", 17 | "label_id": "nightlights", 18 | "name": "NightLights" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": true, 5 | "result": null 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/result_msg_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "result", 4 | "success": false, 5 | "error": { 6 | "code": "invalid_format", 7 | "message": "Message incorrectly formatted: expected str for dictionary value @ data['event_type']. Got 100" 8 | } 9 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Integration/Testdata/service_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "type": "event", 4 | "event": { 5 | "event_type": "call_service", 6 | "data": { 7 | "domain": "light", 8 | "service": "turn_on", 9 | "service_data": { 10 | "entity_id": "light.tomas_rum_fonster" 11 | } 12 | }, 13 | "origin": "LOCAL", 14 | "time_fired": "2019-02-17T12:21:24.802946+00:00", 15 | "context": { 16 | "id": "2bbb14fc617a4e088b3cbbe37d9fbee8", 17 | "user_id": null 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Net/WebSocketClientFactoryTests.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassClient.Tests.Net; 2 | 3 | public class WebSocketClientTests 4 | { 5 | [Fact] 6 | public void TestFactoryReturnCorrectType() 7 | { 8 | WebSocketClientFactory wsFactory = new(Options.Create(new())); 9 | Assert.True(wsFactory.New() is WebSocketClientImpl); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56799/", 7 | "sslPort": 44314 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "NetDaemon.HassClient.Tests": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/README.md: -------------------------------------------------------------------------------- 1 | # Information 2 | 3 | This project can be used to debug client. It is also a showcase how the client can be used as a stand-alone component. -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "System.Net.Http.HttpClient": "Warning" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/DisconnectReason.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client; 2 | 3 | /// 4 | /// The reason websocket connection disconnected 5 | /// 6 | public enum DisconnectReason 7 | { 8 | /// 9 | /// Client disconnected 10 | /// 11 | Client, 12 | 13 | /// 14 | /// Remote host disconnected 15 | /// 16 | Remote, 17 | 18 | /// 19 | /// Remote host disconnected 20 | /// 21 | NotReady, 22 | 23 | /// 24 | /// Error caused by unauthorized token 25 | /// 26 | Unauthorized, 27 | 28 | /// 29 | /// Error caused disconnect 30 | /// 31 | Error 32 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantApiCallException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace NetDaemon.Client.Exceptions; 4 | 5 | [SuppressMessage("", "RCS1194")] 6 | public class HomeAssistantApiCallException : Exception 7 | { 8 | public HttpStatusCode Code { get; private set; } 9 | public HomeAssistantApiCallException(string? message, HttpStatusCode code) : base(message) 10 | { 11 | Code = code; 12 | } 13 | 14 | public HomeAssistantApiCallException() 15 | { 16 | } 17 | 18 | public HomeAssistantApiCallException(string message) : base(message) 19 | { 20 | } 21 | 22 | public HomeAssistantApiCallException(string message, Exception innerException) : base(message, innerException) 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/Exceptions/HomeAssistantConnectionException.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Exceptions; 2 | 3 | // We allow exception not to have default constructors since 4 | // in this case it only makes sense to have a reason 5 | [SuppressMessage("", "RCS1194")] 6 | public class HomeAssistantConnectionException : Exception 7 | { 8 | public HomeAssistantConnectionException(DisconnectReason reason) : base( 9 | $"Home assistant disconnected reason:{reason}") 10 | { 11 | Reason = reason; 12 | } 13 | 14 | public DisconnectReason Reason { get; set; } 15 | 16 | public HomeAssistantConnectionException() 17 | { 18 | } 19 | 20 | public HomeAssistantConnectionException(string message) : base(message) 21 | { 22 | } 23 | 24 | public HomeAssistantConnectionException(string message, Exception innerException) : base(message, innerException) 25 | { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/CommandMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record CommandMessage : HassMessageBase 4 | { 5 | private static readonly JsonSerializerOptions NonIndentingJsonSerializerOptions = new() { WriteIndented = false }; 6 | 7 | [JsonPropertyName("id")] public int Id { get; set; } 8 | 9 | public string GetJsonString() => JsonSerializer.Serialize(this, GetType(), NonIndentingJsonSerializerOptions); 10 | } 11 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassArea.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassArea 4 | { 5 | [JsonPropertyName("name")] public string? Name { get; init; } 6 | [JsonPropertyName("area_id")] public string? Id { get; init; } 7 | 8 | [JsonPropertyName("labels")] public IReadOnlyList Labels { get; init; } = []; 9 | 10 | [JsonPropertyName("floor_id")] public string? FloorId { get; init; } 11 | 12 | [JsonPropertyName("icon")] public string? Icon { get; init; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassAuthResponse.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassAuthResponse : HassMessageBase 4 | { 5 | [JsonPropertyName("ha_version")] public string HaVersion { get; init; } = string.Empty; 6 | } 7 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassConfig.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassConfig 4 | { 5 | [JsonPropertyName("components")] public IReadOnlyCollection? Components { get; init; } 6 | 7 | [JsonPropertyName("config_dir")] public string? ConfigDir { get; init; } 8 | 9 | [JsonPropertyName("elevation")] public int? Elevation { get; init; } 10 | 11 | [JsonPropertyName("latitude")] public float? Latitude { get; init; } 12 | 13 | [JsonPropertyName("location_name")] public string? LocationName { get; init; } 14 | 15 | [JsonPropertyName("longitude")] public float? Longitude { get; init; } 16 | 17 | [JsonPropertyName("time_zone")] public string? TimeZone { get; init; } 18 | 19 | [JsonPropertyName("unit_system")] public HassUnitSystem? UnitSystem { get; init; } 20 | 21 | [JsonPropertyName("version")] public string? Version { get; init; } 22 | 23 | [JsonPropertyName("state")] public string? State { get; init; } 24 | 25 | [JsonPropertyName("whitelist_external_dirs")] 26 | public IReadOnlyCollection? WhitelistExternalDirs { get; init; } 27 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassContext 4 | { 5 | [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; 6 | 7 | [JsonPropertyName("parent_id")] public string? ParentId { get; init; } 8 | 9 | [JsonPropertyName("user_id")] public string? UserId { get; init; } 10 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassConversationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model 2 | { 3 | public record HassEntityConversationOptions 4 | { 5 | [JsonPropertyName("should_expose")] public bool ShouldExpose { get; init; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassEntity.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassEntity 4 | { 5 | [JsonPropertyName("device_id")] public string? DeviceId { get; init; } 6 | 7 | [JsonPropertyName("entity_id")] public string? EntityId { get; init; } 8 | 9 | [JsonPropertyName("area_id")] public string? AreaId { get; init; } 10 | 11 | [JsonPropertyName("name")] public string? Name { get; init; } 12 | 13 | [JsonPropertyName("icon")] public string? Icon { get; init; } 14 | 15 | [JsonPropertyName("platform")] public string? Platform { get; init; } 16 | 17 | [JsonPropertyName("labels")] public IReadOnlyList Labels { get; init; } = []; 18 | 19 | [JsonPropertyName("options")]public HassEntityOptions? Options { get; init; } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassEntityOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model 2 | { 3 | public record HassEntityOptions 4 | { 5 | [JsonPropertyName("conversation")] public HassEntityConversationOptions? Conversation { get; init; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassError.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassError 4 | { 5 | [JsonPropertyName("code")] public object? Code { get; init; } 6 | 7 | [JsonPropertyName("message")] public string? Message { get; init; } 8 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassEvent.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.Client.Common.HomeAssistant.Model; 2 | 3 | namespace NetDaemon.Client.HomeAssistant.Model; 4 | 5 | public record HassEvent 6 | { 7 | [JsonPropertyName("data")] public JsonElement? DataElement { get; init; } 8 | 9 | [JsonPropertyName("variables")] public HassVariable? Variables { get; init; } 10 | 11 | [JsonPropertyName("event_type")] public string EventType { get; init; } = string.Empty; 12 | 13 | [JsonPropertyName("origin")] public string Origin { get; init; } = string.Empty; 14 | 15 | [JsonPropertyName("time_fired")] public DateTime? TimeFired { get; init; } 16 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassFloor.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassFloor 4 | { 5 | [JsonPropertyName("level")] public short? Level { get; init; } 6 | 7 | [JsonPropertyName("icon")] public string? Icon { get; init; } 8 | 9 | [JsonPropertyName("floor_id")] public string? Id { get; init; } 10 | 11 | [JsonPropertyName("name")] public string? Name { get; init; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassLabel.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassLabel 4 | { 5 | [JsonPropertyName("color")] public string? Color { get; init; } 6 | 7 | [JsonPropertyName("description")] public string? Description { get; init; } 8 | 9 | [JsonPropertyName("icon")] public string? Icon { get; init; } 10 | 11 | [JsonPropertyName("label_id")] public string? Id { get; init; } 12 | 13 | [JsonPropertyName("name")] public string? Name { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | public record HassMessage : HassMessageBase 4 | { 5 | [JsonPropertyName("event")] public HassEvent? Event { get; init; } 6 | 7 | [JsonPropertyName("id")] public int Id { get; init; } 8 | 9 | [JsonPropertyName("message")] public string? Message { get; init; } 10 | 11 | [JsonPropertyName("result")] public JsonElement? ResultElement { get; init; } 12 | 13 | [JsonPropertyName("success")] public bool? Success { get; init; } 14 | 15 | [JsonPropertyName("error")] public HassError? Error { get; init; } 16 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassMessageBase.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassMessageBase 4 | { 5 | [JsonPropertyName("type")] public string Type { get; init; } = string.Empty; 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassServiceEventData.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassServiceEventData 4 | { 5 | [JsonPropertyName("domain")] public string Domain { get; init; } = string.Empty; 6 | 7 | [JsonPropertyName("service")] public string Service { get; init; } = string.Empty; 8 | 9 | [JsonPropertyName("service_data")] public JsonElement? ServiceDataElement { get; init; } 10 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassServiceResult.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassServiceResult 4 | { 5 | [JsonPropertyName("context")] public HassContext? Context { get; init; } 6 | [JsonPropertyName("response")] public JsonElement? Response { get; init; } 7 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassState.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassState 4 | { 5 | [JsonPropertyName("attributes")] public JsonElement? AttributesJson { get; init; } 6 | 7 | public IReadOnlyDictionary? Attributes 8 | { 9 | get => AttributesJson?.Deserialize>() ?? []; 10 | init => AttributesJson = value.ToJsonElement(); 11 | } 12 | 13 | [JsonPropertyName("entity_id")] public string EntityId { get; init; } = ""; 14 | 15 | [JsonPropertyName("last_changed")] public DateTime LastChanged { get; init; } = DateTime.MinValue; 16 | [JsonPropertyName("last_updated")] public DateTime LastUpdated { get; init; } = DateTime.MinValue; 17 | [JsonPropertyName("state")] public string? State { get; init; } = ""; 18 | [JsonPropertyName("context")] public HassContext? Context { get; init; } 19 | 20 | public T? AttributesAs() 21 | { 22 | return AttributesJson.HasValue ? AttributesJson.Value.Deserialize() : default; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassStateChangedEventData.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassStateChangedEventData 4 | { 5 | [JsonPropertyName("entity_id")] public string EntityId { get; init; } = ""; 6 | 7 | [JsonPropertyName("new_state")] public HassState? NewState { get; init; } 8 | 9 | [JsonPropertyName("old_state")] public HassState? OldState { get; init; } 10 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassUnitSystem.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record HassUnitSystem 4 | { 5 | [JsonPropertyName("length")] public string? Length { get; init; } 6 | 7 | [JsonPropertyName("mass")] public string? Mass { get; init; } 8 | 9 | [JsonPropertyName("temperature")] public string? Temperature { get; init; } 10 | 11 | [JsonPropertyName("volume")] public string? Volume { get; init; } 12 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/HassVariable.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Common.HomeAssistant.Model; 2 | 3 | public record HassVariable 4 | { 5 | [JsonPropertyName("trigger")] public JsonElement? TriggerElement { get; init; } 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/InputBooleanHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record InputBooleanHelper 4 | { 5 | [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; 6 | [JsonPropertyName("icon")] public string? Icon { get; init; } 7 | [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/HomeAssistant/Model/InputNumberHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.HomeAssistant.Model; 2 | 3 | public record InputNumberHelper 4 | { 5 | [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; 6 | [JsonPropertyName("icon")] public string? Icon { get; init; } 7 | [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; 8 | [JsonPropertyName("min")] public double Min { get; init; } 9 | [JsonPropertyName("max")] public double Max { get; init; } 10 | [JsonPropertyName("step")] public double? Step { get; init; } 11 | [JsonPropertyName("initial")] public double? Initial { get; init; } 12 | [JsonPropertyName("mode")] public string? Mode { get; init; } 13 | [JsonPropertyName("unit_of_measurement")] public string? UnitOfMeasurement { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/IHomeAssistantApiManager.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client; 2 | 3 | /// 4 | /// Handle home assistant API calls using json over http 5 | /// 6 | public interface IHomeAssistantApiManager 7 | { 8 | /// 9 | /// Get to Home Assistant API 10 | /// 11 | /// relative path 12 | /// cancellation token 13 | /// Return type (json serializable) 14 | Task GetApiCallAsync(string apiPath, CancellationToken cancelToken); 15 | 16 | /// 17 | /// Post to Home Assistant API 18 | /// 19 | /// relative path 20 | /// cancellation token 21 | /// data being sent 22 | /// Return type (json serializable) 23 | public Task PostApiCallAsync(string apiPath, CancellationToken cancelToken, object? data = null); 24 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Common/IHomeAssistantClient.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client; 2 | 3 | /// 4 | /// HomeAssistantClient 5 | /// 6 | public interface IHomeAssistantClient 7 | { 8 | /// 9 | /// Connect to Home Assistant 10 | /// 11 | /// The host name 12 | /// Network port 13 | /// Set true to use ssl 14 | /// The access token to use 15 | /// Cancellation token 16 | Task ConnectAsync(string host, int port, bool ssl, string token, 17 | CancellationToken cancelToken); 18 | 19 | /// 20 | /// Connect to Home Assistant 21 | /// 22 | /// The host name 23 | /// Network port 24 | /// Set true to use ssl 25 | /// The access token to use 26 | /// The relative websocket path to use connecting 27 | /// Cancellation token 28 | Task ConnectAsync(string host, int port, bool ssl, string token, string websocketPath, 29 | CancellationToken cancelToken); 30 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Extensions/CancellationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Extensions; 2 | 3 | internal static class CancellationTokenExtensions 4 | { 5 | /// 6 | /// Allows using a Cancellation Token as if it were a task. 7 | /// 8 | /// The cancellation token. 9 | /// A task that can be canceled, but never completed. 10 | public static Task AsTask(this CancellationToken cancellationToken) 11 | { 12 | return AsTask(cancellationToken); 13 | } 14 | 15 | /// Allows using a Cancellation Token as if it were a task. 16 | /// 17 | /// The cancellation token. 18 | /// A task that can be canceled, but never completed. 19 | private static Task AsTask(this CancellationToken cancellationToken) 20 | { 21 | var tcs = new TaskCompletionSource(); 22 | cancellationToken.Register(() => tcs.TrySetCanceled(), false); 23 | return tcs.Task; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Extensions/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Extensions; 2 | 3 | internal static class JsonExtensions 4 | { 5 | public static JsonElement? ToJsonElement(this T source, JsonSerializerOptions? options = null) 6 | { 7 | if (source == null) return null; 8 | return JsonSerializer.SerializeToElement(source, options); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Helpers/AsyncLazy.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Helpers; 2 | 3 | public class AsyncLazy(Func> taskFactory) : Lazy>(() 4 | => Task.Factory.StartNew(taskFactory).Unwrap()); 5 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Helpers/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Helpers; 2 | 3 | internal static class HttpHelper 4 | { 5 | [SuppressMessage("", "CA2000")] 6 | public static HttpClient CreateHttpClient() 7 | { 8 | return new HttpClient(CreateHttpMessageHandler()); 9 | } 10 | 11 | public static HttpMessageHandler CreateHttpMessageHandler(IServiceProvider? serviceProvider = null) 12 | { 13 | var settings = serviceProvider?.GetService>()?.Value; 14 | var bypassCertificateErrors = settings?.InsecureBypassCertificateErrors ?? false; 15 | return !bypassCertificateErrors 16 | ? new HttpClientHandler() 17 | : CreateHttpMessageHandler(); 18 | } 19 | 20 | private static HttpMessageHandler CreateHttpMessageHandler() 21 | { 22 | return new HttpClientHandler 23 | { 24 | ServerCertificateCustomValidationCallback = (_, _, _, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None || true 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Helpers/VersionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Helpers; 2 | 3 | internal static partial class VersionHelper 4 | { 5 | public static Version ReplaceBeta(string version) 6 | => Version.Parse(BetaVersion().Replace(version, ".0")); 7 | 8 | [GeneratedRegex("\\.0b\\d+$")] 9 | private static partial Regex BetaVersion(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/CallExecuteScriptCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record CallExecuteScriptCommand : CommandMessage 4 | { 5 | public CallExecuteScriptCommand() 6 | { 7 | Type = "execute_script"; 8 | } 9 | 10 | [JsonPropertyName("sequence")] public object[] Sequence { get; init; } = default!; 11 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/CallServiceCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record CallServiceCommand : CommandMessage 4 | { 5 | public CallServiceCommand() 6 | { 7 | Type = "call_service"; 8 | } 9 | 10 | [JsonPropertyName("domain")] public string Domain { get; init; } = string.Empty; 11 | 12 | [JsonPropertyName("service")] public string Service { get; init; } = string.Empty; 13 | 14 | [JsonPropertyName("service_data")] public object? ServiceData { get; init; } 15 | 16 | [JsonPropertyName("target")] public HassTarget? Target { get; init; } 17 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/CreateHelperCommandBase.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record CreateHelperCommandBase : CommandMessage 4 | { 5 | public CreateHelperCommandBase(string helperType) 6 | { 7 | Type = $"{helperType}/create"; 8 | } 9 | [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; 10 | [JsonPropertyName("icon")] public string Icon { get; init; } = string.Empty; 11 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/CreateInputBooleanHelperCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record CreateInputBooleanHelperCommand : CommandMessage 4 | { 5 | public CreateInputBooleanHelperCommand() 6 | { 7 | Type = "input_boolean/create"; 8 | } 9 | 10 | [JsonPropertyName("name")] public required string Name { get; init; } 11 | } 12 | 13 | internal record DeleteInputBooleanHelperCommand : CommandMessage 14 | { 15 | public DeleteInputBooleanHelperCommand() 16 | { 17 | Type = "input_boolean/delete"; 18 | } 19 | 20 | [JsonPropertyName("input_boolean_id")] public required string InputBooleanId { get; init; } = string.Empty; 21 | } 22 | 23 | internal record ListInputBooleanHelperCommand : CommandMessage 24 | { 25 | public ListInputBooleanHelperCommand() 26 | { 27 | Type = "input_boolean/list"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/SimpleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record SimpleCommand : CommandMessage 4 | { 5 | public SimpleCommand(string type) 6 | { 7 | Type = type; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/SubscribeEventCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record SubscribeEventCommand : CommandMessage 4 | { 5 | public SubscribeEventCommand() 6 | { 7 | Type = "subscribe_events"; 8 | } 9 | 10 | [JsonPropertyName("event_type")] public string? EventType { get; init; } 11 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/SubscribeTriggerCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record SubscribeTriggerCommand : CommandMessage 4 | { 5 | public SubscribeTriggerCommand(object trigger) 6 | { 7 | Type = "subscribe_trigger"; 8 | Trigger = trigger; 9 | } 10 | 11 | [JsonPropertyName("trigger")] 12 | public object Trigger { get; init; } 13 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/SupportedFeaturesCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record SupportedFeaturesCommand : CommandMessage 4 | { 5 | public SupportedFeaturesCommand() 6 | { 7 | Type = "supported_features"; 8 | } 9 | 10 | [JsonPropertyName("features")] public Features? Features { get; init; } 11 | } 12 | 13 | internal record Features 14 | { 15 | [JsonPropertyName("coalesce_messages")] public short? CoalesceMessages { get; init; } = 1; 16 | } 17 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Commands/UnsubscribeEventsCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Commands; 2 | 3 | internal record UnsubscribeEventsCommand : CommandMessage 4 | { 5 | public UnsubscribeEventsCommand(int subscriptionId) 6 | { 7 | Type = "unsubscribe_events"; 8 | Subscription = subscriptionId; 9 | } 10 | 11 | [JsonPropertyName("subscription")] public int Subscription { get; init; } 12 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistant/Messages/HassAuthMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.HomeAssistant.Messages; 2 | 3 | internal record HassAuthMessage : HassMessageBase 4 | { 5 | public HassAuthMessage() 6 | { 7 | Type = "auth"; 8 | } 9 | 10 | [JsonPropertyName("access_token")] public string AccessToken { get; init; } = string.Empty; 11 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/HomeAssistantConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal; 2 | 3 | internal class HomeAssistantConnectionFactory(ILogger logger, 4 | IHomeAssistantApiManager apiManager) : IHomeAssistantConnectionFactory 5 | { 6 | public IHomeAssistantConnection New(IWebSocketClientTransportPipeline transportPipeline) 7 | { 8 | return new HomeAssistantConnection(logger, transportPipeline, apiManager); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/IHomeAssistantConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal; 2 | 3 | internal interface IHomeAssistantConnectionFactory 4 | { 5 | IHomeAssistantConnection New(IWebSocketClientTransportPipeline transportPipeline); 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/ITransportPipeline.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | /// 4 | /// The pipeline makes a transport layer on top of WebSocketClient. 5 | /// This pipeline handles json serialization 6 | /// 7 | internal interface IWebSocketClientTransportPipeline : IAsyncDisposable 8 | { 9 | /// 10 | /// State of the underlying websocket 11 | /// 12 | WebSocketState WebSocketState { get; } 13 | 14 | /// 15 | /// Gets next message from pipeline 16 | /// 17 | ValueTask GetNextMessagesAsync(CancellationToken cancellationToken) where T : class; 18 | 19 | /// 20 | /// Sends a message to the pipeline 21 | /// 22 | /// 23 | /// Cancellation token 24 | Task SendMessageAsync(T message, CancellationToken cancellationToken) where T : class; 25 | 26 | /// 27 | /// Close the pipeline, it will also close the underlying websocket 28 | /// 29 | Task CloseAsync(); 30 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/IWebSocketClient.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | internal interface IWebSocketClient : IAsyncDisposable 4 | { 5 | WebSocketState State { get; } 6 | WebSocketCloseStatus? CloseStatus { get; } 7 | 8 | Task ConnectAsync(Uri uri, CancellationToken cancel); 9 | 10 | Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, 11 | CancellationToken cancellationToken); 12 | 13 | Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, 14 | CancellationToken cancellationToken); 15 | 16 | Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, 17 | CancellationToken cancellationToken); 18 | 19 | ValueTask SendAsync(ReadOnlyMemory buffer, WebSocketMessageType messageType, bool endOfMessage, 20 | CancellationToken cancellationToken); 21 | 22 | ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken); 23 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/IWebSocketClientFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | /// 4 | /// Factory for Client Websocket. Implement to use for mockups 5 | /// 6 | internal interface IWebSocketClientFactory 7 | { 8 | IWebSocketClient New(); 9 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/IWebSocketClientTransportPipelineFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | internal interface IWebSocketClientTransportPipelineFactory 4 | { 5 | IWebSocketClientTransportPipeline New(IWebSocketClient webSocketClient); 6 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/WebSocketClientFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | internal class WebSocketClientFactory : IWebSocketClientFactory 4 | { 5 | private readonly HomeAssistantSettings _settings; 6 | 7 | public WebSocketClientFactory( 8 | IOptions settings 9 | ) 10 | { 11 | ArgumentNullException.ThrowIfNull(settings); 12 | _settings = settings.Value; 13 | } 14 | public IWebSocketClient New() 15 | { 16 | return new WebSocketClientImpl(_settings.InsecureBypassCertificateErrors); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Client/NetDaemon.HassClient/Internal/Net/WebSocketTransportPipelineFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Client.Internal.Net; 2 | 3 | internal class WebSocketClientTransportPipelineFactory : IWebSocketClientTransportPipelineFactory 4 | { 5 | public IWebSocketClientTransportPipeline New(IWebSocketClient webSocketClient) 6 | { 7 | if (webSocketClient.State != WebSocketState.Open) 8 | throw new ApplicationException("Unexpected state of WebSocketClient, should be 'Open'"); 9 | 10 | return new WebSocketClientTransportPipeline(webSocketClient); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Logging/Common/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using NetDaemon.Extensions.Logging.Internal; 3 | using Serilog; 4 | 5 | namespace NetDaemon.Extensions.Logging; 6 | 7 | /// 8 | /// Adds extension to the IHostBuilder to add default NetDaemon logging capabilities 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds default logging capabilities for NetDaemon 14 | /// 15 | /// 16 | public static IHostBuilder UseNetDaemonDefaultLogging(this IHostBuilder builder) 17 | { 18 | return builder.UseSerilog((context, loggerConfiguration) => 19 | SerilogConfigurator.Configure(loggerConfiguration, context.HostingEnvironment)); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Logging/Internal/LoggingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace NetDaemon.Extensions.Logging.Internal; 4 | 5 | internal class LoggingConfiguration 6 | { 7 | public LogLevel? LogLevel { get; set; } 8 | public string ConsoleThemeType { get; set; } = "Ansi"; 9 | } 10 | 11 | [SuppressMessage("", "CA1812")] 12 | internal class LogLevel 13 | { 14 | public string Default { get; set; } = "Info"; 15 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttConnectionException.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.MqttEntityManager.Exceptions; 2 | 3 | /// 4 | /// MQTT connection failed 5 | /// 6 | public class MqttConnectionException : Exception 7 | { 8 | /// 9 | /// MQTT connection failed 10 | /// 11 | /// 12 | public MqttConnectionException(string msg) : base(msg) 13 | {} 14 | 15 | /// 16 | /// MQTT connection failed 17 | /// 18 | /// 19 | /// 20 | public MqttConnectionException(string msg, Exception innerException) : base(msg, innerException) 21 | {} 22 | 23 | /// 24 | /// MQTT connection failed 25 | /// 26 | public MqttConnectionException() 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttPublishException.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.MqttEntityManager.Exceptions; 2 | 3 | /// 4 | /// Failed to publish a message to MQTT 5 | /// 6 | public class MqttPublishException : Exception 7 | { 8 | /// 9 | /// Failed to publish a message to MQTT 10 | /// 11 | /// 12 | public MqttPublishException(string msg) : base(msg) 13 | {} 14 | 15 | /// 16 | /// Failed to publish a message to MQTT 17 | /// 18 | /// 19 | /// 20 | public MqttPublishException(string msg, Exception innerException) : base(msg, innerException) 21 | {} 22 | 23 | /// 24 | /// Failed to publish a message to MQTT 25 | /// 26 | public MqttPublishException() 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Helpers/ByteArrayHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager.Helpers; 4 | 5 | /// 6 | /// Helpers for byte arrays 7 | /// 8 | internal static class ByteArrayHelper 9 | { 10 | /// 11 | /// Convert a byte array to a string, or to an empty string if the array is not valid UTF8 12 | /// 13 | /// 14 | /// 15 | public static string SafeToString(byte[]? array) 16 | { 17 | try 18 | { 19 | if (array == null || array.Length == 0) 20 | return ""; 21 | 22 | return Encoding.UTF8.GetString(array); 23 | } 24 | catch (Exception) 25 | { 26 | return ""; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Helpers/EntityCreationPayloadHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using NetDaemon.Extensions.MqttEntityManager.Models; 4 | 5 | namespace NetDaemon.Extensions.MqttEntityManager.Helpers; 6 | 7 | /// 8 | /// Helpers around EntityCreationPayload 9 | /// 10 | internal class EntityCreationPayloadHelper 11 | { 12 | /// 13 | /// Merge an optional dynamic set of parameters with the concrete payload 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | internal static string Merge(EntityCreationPayload concreteOptions, object? additionalOptions) 20 | { 21 | var concreteJson = JsonSerializer.SerializeToNode(concreteOptions)?.AsObject() 22 | ?? throw new JsonException("Unable to convert concrete config to JsonObject"); 23 | 24 | if (additionalOptions != null) 25 | { 26 | JsonObject? dynamicJson = JsonSerializer.SerializeToNode(additionalOptions) as JsonObject; 27 | concreteJson.AddRange(dynamicJson); 28 | } 29 | 30 | return concreteJson.ToJsonString(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Helpers/EntityIdParser.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.MqttEntityManager.Helpers; 2 | 3 | /// 4 | /// Parsing utilities for entity IDs 5 | /// 6 | internal static class EntityIdParser 7 | { 8 | /// 9 | /// Extract the domain and identifier from an entity ID string 10 | /// 11 | /// Entity ID in the format "domain.identifier" 12 | /// 13 | /// If entityId is not supplied or is an invalid 14 | /// format 15 | public static (string domain, string identifier) Extract(string entityId) 16 | { 17 | if (string.IsNullOrWhiteSpace(entityId)) 18 | throw new ArgumentException($"{nameof(entityId)} cannot be null or whitespace",nameof(entityId)); 19 | 20 | var components = entityId.Split('.', 2); 21 | if (components.Length != 2 || 22 | string.IsNullOrWhiteSpace(components[0]) || 23 | string.IsNullOrWhiteSpace(components[1])) 24 | throw new ArgumentException( 25 | $"The {nameof(entityId)} should be of the format 'domain.identifier'. The value was {entityId}"); 26 | 27 | return (components[0], components[1]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Helpers/IMqttFactoryWrapper.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet.Extensions.ManagedClient; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager.Helpers; 4 | 5 | /// 6 | /// Testable wrapper around IMqttFactory 7 | /// 8 | internal interface IMqttFactoryWrapper 9 | { 10 | /// 11 | /// Return a managed MQTT client, either from the original factory or a pre-supplied one 12 | /// 13 | /// 14 | IManagedMqttClient CreateManagedMqttClient(); 15 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Helpers/JsonNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace NetDaemon.Extensions.MqttEntityManager.Helpers; 5 | 6 | /// 7 | /// Extensions for JsonNode and inheritors 8 | /// 9 | internal static class JsonNodeExtensions 10 | { 11 | /// 12 | /// For a given JsonObject, merge a second set of values, replacing any pre-existing properties in 13 | /// the target object 14 | /// 15 | /// 16 | /// 17 | public static void AddRange(this JsonObject target, JsonObject? toMerge) 18 | { 19 | if (toMerge == null) 20 | return; 21 | 22 | foreach (var kvp in toMerge) 23 | { 24 | var k = kvp.Key; 25 | var v = kvp.Value; 26 | 27 | target.Remove(k); 28 | 29 | target.Add(new(k, v?.Deserialize())); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/IAssuredMqttConnection.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet.Extensions.ManagedClient; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager; 4 | 5 | /// 6 | /// Wrapper to assure an MQTT connection 7 | /// 8 | internal interface IAssuredMqttConnection 9 | { 10 | /// 11 | /// Ensures that the MQTT client is available 12 | /// 13 | Task GetClientAsync(); 14 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMessageSender.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet.Protocol; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager; 4 | 5 | /// 6 | /// Interface to send messages to MQTT 7 | /// 8 | internal interface IMessageSender 9 | { 10 | /// 11 | /// Send a message for the given payload to the MQTT topic 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | Task SendMessageAsync(string topic, string payload, bool retain, MqttQualityOfServiceLevel qos); 19 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMessageSubscriber.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.MqttEntityManager; 2 | 3 | internal interface IMessageSubscriber 4 | { 5 | /// 6 | /// Receive a message from the given topic 7 | /// 8 | /// 9 | Task> SubscribeTopicAsync(string topic); 10 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMqttClientOptionsFactory.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet.Extensions.ManagedClient; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager; 4 | 5 | /// 6 | /// Represents a factory for creating MQTT client options. 7 | /// 8 | public interface IMqttClientOptionsFactory 9 | { 10 | /// 11 | /// Creates the client options for MQTT connection from the supplied configuration. 12 | /// /// 13 | /// The MQTT configuration. 14 | /// The managed MQTT client options. 15 | ManagedMqttClientOptions CreateClientOptions(MqttConfiguration mqttConfig); 16 | } 17 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMqttFactory.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet.Extensions.ManagedClient; 2 | 3 | namespace NetDaemon.Extensions.MqttEntityManager; 4 | 5 | /// 6 | /// MqttNet removed the IMqttFactory interface at v4 which breaks our DI 7 | /// So this is a factory for the MqttFactory to satisfy our use case 8 | /// 9 | internal interface IMqttFactory 10 | { 11 | /// 12 | /// Create a Managed Mqtt Client 13 | /// 14 | /// 15 | IManagedMqttClient CreateManagedMqttClient(); 16 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/Models/EntityCreationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.MqttEntityManager; 2 | 3 | /// 4 | /// Parameters to create an entity 5 | /// 6 | /// Optional device class - see HA integration documentation 7 | /// Optional unique ID to use - if not specified then one will be generated 8 | /// Optional name of the entity 9 | /// Optional payload to set the entity available 10 | /// Optional payload to set the entity not-available 11 | /// Optional payload to set the command on 12 | /// Optional payload to set the command off 13 | /// Optionally persist the entity over HA restarts, default is true 14 | public record EntityCreationOptions( 15 | string? DeviceClass = null, 16 | string? UniqueId = null, 17 | string? Name = null, 18 | string? PayloadAvailable = null, 19 | string? PayloadNotAvailable = null, 20 | string? PayloadOn = null, 21 | string? PayloadOff = null, 22 | bool Persist = true 23 | ); -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttFactoryFactory.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet; 2 | using MQTTnet.Extensions.ManagedClient; 3 | 4 | namespace NetDaemon.Extensions.MqttEntityManager; 5 | 6 | /// 7 | /// MqttNet removed the IMqttFactory interface at v4 which breaks our DI 8 | /// So this is a factory for the MqttFactory to satisfy our use case 9 | /// 10 | internal class MqttFactoryFactory : IMqttFactory 11 | { 12 | /// 13 | /// Create a Managed Mqtt Client 14 | /// 15 | /// 16 | public IManagedMqttClient CreateManagedMqttClient() 17 | { 18 | return new MqttFactory().CreateManagedMqttClient(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Scheduling.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.xUnit1030.severity = none -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Scheduling.Tests/Scheduling/FakeLocalTimeZone.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace NetDaemon.Extensions.Scheduling.Tests; 4 | 5 | /// 6 | /// Helper class to fake timezone for localtime 7 | /// 8 | internal sealed class FakeLocalTimeZone : IDisposable 9 | { 10 | private readonly TimeZoneInfo _actualLocalTimeZoneInfo; 11 | 12 | private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo) 13 | { 14 | // Fake timezone by using reflection of private fields, this might break in the future 15 | var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static); 16 | var cachedData = info!.GetValue(null); 17 | 18 | var field = cachedData!.GetType().GetField("_localTimeZone", BindingFlags.NonPublic | BindingFlags.Instance); 19 | field!.SetValue(cachedData, timeZoneInfo); 20 | } 21 | 22 | public FakeLocalTimeZone(TimeZoneInfo timeZoneInfo) 23 | { 24 | _actualLocalTimeZoneInfo = TimeZoneInfo.Local; 25 | SetLocalTimeZone(timeZoneInfo); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | SetLocalTimeZone(_actualLocalTimeZoneInfo); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Scheduling/DependencyInjectionSetup.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace NetDaemon.Extensions.Scheduler; 6 | 7 | /// 8 | /// Implements dependency injection for the scheduler 9 | /// 10 | public static class DependencyInjectionSetup 11 | { 12 | /// 13 | /// Adds scheduling capabilities through dependency injection 14 | /// 15 | /// Provided service collection 16 | public static IServiceCollection AddNetDaemonScheduler(this IServiceCollection services) 17 | { 18 | services.AddScoped(); 19 | services.AddScoped(s => new DisposableScheduler(DefaultScheduler.Instance.WrapWithLogger(s.GetRequiredService>()))); 20 | return services; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Scheduling/SchedulerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using System.Reactive.Linq; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace NetDaemon.Extensions.Scheduler; 6 | 7 | /// 8 | /// Extension Methods for IScheduler 9 | /// 10 | public static class SchedulerExtensions 11 | { 12 | /// 13 | /// Schedules an action every (timespan) 14 | /// 15 | /// The Scheduler to use 16 | /// The period to schedule 17 | /// The time to start the schedule 18 | /// Action to run 19 | public static IDisposable RunEvery(this IScheduler scheduler, TimeSpan period, DateTimeOffset startTime, Action action) 20 | { 21 | return Observable.Timer(startTime, period, scheduler).Subscribe(_ => action()); 22 | } 23 | 24 | internal static IScheduler WrapWithLogger(this IScheduler scheduler, ILogger logger) => 25 | scheduler.Catch(e => 26 | { 27 | logger.LogError(e, "Error in scheduled task"); 28 | return true; // Marks the exception as handled 29 | }); 30 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Tts/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Diagnostics.CodeAnalysis; 3 | global using System.Threading; 4 | global using System.Threading.Channels; 5 | global using System.Threading.Tasks; 6 | global using Microsoft.Extensions.Logging; 7 | global using NetDaemon.Client; 8 | global using NetDaemon.Client.HomeAssistant.Extensions; 9 | global using NetDaemon.Client.HomeAssistant.Model; -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Tts/HostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using NetDaemon.Extensions.Tts.Internal; 4 | 5 | namespace NetDaemon.Extensions.Tts; 6 | 7 | /// 8 | /// Extension methods for text-to-speech 9 | /// 10 | public static class HostBuilderExtensions 11 | { 12 | /// 13 | /// Use the text-to-speech engine of NetDaemon 14 | /// 15 | /// Builder 16 | public static IHostBuilder UseNetDaemonTextToSpeech(this IHostBuilder hostBuilder) 17 | { 18 | hostBuilder.ConfigureServices(services => 19 | { 20 | services.AddSingleton(); 21 | services.AddSingleton(s => s.GetRequiredService()); 22 | }); 23 | return hostBuilder; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Extensions/NetDaemon.Extensions.Tts/Internal/TtsMessage.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.Extensions.Tts.Internal; 2 | 3 | internal record TtsMessage 4 | { 5 | public string EntityId { get; init; } = string.Empty; 6 | public string Message { get; init; } = string.Empty; 7 | public string Service { get; init; } = string.Empty; 8 | public bool? Cache { get; init; } 9 | public object? Options { get; init; } = string.Empty; 10 | public string? Language { get; init; } 11 | } 12 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/.editorconfig: -------------------------------------------------------------------------------- 1 |  [*.cs] 2 | # Warning CA1862 : Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862) 3 | dotnet_diagnostic.CA1862.severity = none 4 | # Warning CA1860 : Prefer comparing 'Count' to 0 rather than using 'Any()' 5 | dotnet_diagnostic.CA1860.severity = none 6 | # Warning CA1869 : Avoid creating a new 'JsonSerializerOptions' instance for every 7 | dotnet_diagnostic.CA1869.severity = none 8 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGenerationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.CodeGenerator; 2 | 3 | record CodeGenerationSettings 4 | { 5 | public string OutputFile { get; init; } = "HomeAssistantGenerated.cs"; 6 | public string OutputFolder { get; init; } = "NetDaemonCodegen"; 7 | public string Namespace { get; init; } = "HomeAssistantGenerated"; 8 | public bool UseAttributeBaseClasses { get; set; } // For now we default to false for backwards compat. Later we might default to true 9 | public bool GenerateOneFilePerEntity { get; set; } 10 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/Extensions/CodeGeneratorExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.CodeGenerator.Extensions 2 | { 3 | internal static class CodeGeneratorExtensions 4 | { 5 | public static string GetClassName(this CompilationUnitSyntax compilationUnit) 6 | { 7 | if (compilationUnit.DescendantNodes().OfType().Any()) 8 | return compilationUnit.DescendantNodes().OfType().First().Identifier.ToString(); 9 | 10 | if (compilationUnit.DescendantNodes().OfType().Any()) 11 | return compilationUnit.DescendantNodes().OfType().First().Identifier.ToString(); 12 | 13 | if (compilationUnit.DescendantNodes().OfType().Any()) 14 | return compilationUnit.DescendantNodes().OfType().First().Identifier.ToString(); 15 | 16 | return string.Empty; 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Linq; 4 | global using System.Text.Json; 5 | global using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | global using NetDaemon.HassModel.CodeGenerator; 7 | global using NetDaemon.HassModel.CodeGenerator.Model; 8 | global using NetDaemon.HassModel.CodeGenerator.Helpers; 9 | global using NetDaemon.HassModel.CodeGenerator.Extensions; 10 | global using NetDaemon.HassModel.Entities; 11 | global using Microsoft.CodeAnalysis; 12 | global using static NetDaemon.HassModel.CodeGenerator.Helpers.NamingHelper; 13 | global using static NetDaemon.HassModel.CodeGenerator.Helpers.SyntaxFactoryHelper; 14 | 15 | // This is needed to allow integration tests to run code generation and parsing without major refactoring 16 | using System.Runtime.CompilerServices; 17 | [assembly:InternalsVisibleTo("NetDaemon.Tests.Integration")] 18 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/EntityIdHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.CodeGenerator.Helpers; 2 | 3 | internal static class EntityIdHelper 4 | { 5 | public static readonly string[] NumericDomains = ["input_number", "number", "proximity"]; 6 | public static readonly string[] MixedDomains = ["sensor"]; 7 | 8 | public static string GetDomain(string str) 9 | { 10 | return str[..str.IndexOf('.', StringComparison.InvariantCultureIgnoreCase)]; 11 | } 12 | 13 | public static string GetEntity(string str) 14 | { 15 | return str[(str.IndexOf('.', StringComparison.InvariantCultureIgnoreCase) + 1)..]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/VersionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Helpers; 4 | 5 | #pragma warning disable CA1303 6 | 7 | /// 8 | /// Helper class for managing NetDaemon version tasks 9 | /// 10 | public static class VersionHelper 11 | { 12 | /// 13 | /// Returns current version of NetDaemon 14 | /// 15 | public static Version GeneratorVersion { get; } = 16 | Assembly.GetAssembly(typeof(Generator))!.GetName().Version!; 17 | 18 | /// 19 | /// Pretty prints version information to console 20 | /// 21 | public static void PrintVersion() 22 | { 23 | Console.Write("nd-codegen version: "); 24 | Console.ForegroundColor = ConsoleColor.Green; 25 | Console.WriteLine(GeneratorVersion.ToString(3)); 26 | Console.ResetColor(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/EntityMetaData/ClrTypeJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator; 4 | 5 | /// 6 | /// Json(De)Serializes a System.Type using a 'friendly name' 7 | /// 8 | internal class ClrTypeJsonConverter : JsonConverter 9 | { 10 | public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | var typeName = reader.GetString(); 13 | if (typeName == null) return null; 14 | 15 | return Type.GetType(typeName) ?? throw new InvalidOperationException($@"Type {typeName} is not found when deserializing"); 16 | } 17 | 18 | public override void Write(Utf8JsonWriter writer, Type? value, JsonSerializerOptions options) 19 | { 20 | writer.WriteStringValue(value?.ToString()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/EntityMetaData/NullableBoolJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator; 4 | 5 | //Based on: https://stackoverflow.com/a/68685773 6 | internal class NullableBoolJsonConverter : JsonConverter 7 | { 8 | public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) => 9 | writer.WriteBooleanValue(value ?? false); 10 | 11 | public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 12 | reader.TokenType switch 13 | { 14 | JsonTokenType.True => true, 15 | JsonTokenType.False => false, 16 | JsonTokenType.String => bool.TryParse(reader.GetString(), out var b) ? b : throw new JsonException(), 17 | JsonTokenType.Number => reader.TryGetInt64(out long l) ? Convert.ToBoolean(l) : reader.TryGetDouble(out double d) && Convert.ToBoolean(d), 18 | _ => throw new JsonException(), 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/HassService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | internal record HassService 6 | { 7 | public string Service { get; init; } = ""; // cannot be required because the JsonSerializer will complain 8 | public string? Description { get; init; } 9 | 10 | [JsonIgnore] 11 | public IReadOnlyCollection? Fields { get; init; } 12 | public TargetSelector? Target { get; init; } 13 | public Response? Response { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/HassServiceDomain.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.CodeGenerator.Model; 2 | 3 | internal record HassServiceDomain 4 | { 5 | public required string Domain { get; init; } 6 | public required IReadOnlyCollection Services { get; init; } 7 | } 8 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/HassServiceField.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | internal record HassServiceField 6 | { 7 | public string Field { get; init; } = ""; // cannot be required because the JsonSerializer will complain 8 | public string? Description { get; init; } 9 | [JsonConverter(typeof(NullableBoolJsonConverter))] 10 | public bool? Required { get; init; } 11 | public object? Example { get; init; } 12 | 13 | [JsonConverter(typeof(SelectorConverter))] 14 | public Selector? Selector { get; init; } 15 | } 16 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/Response.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json.Serialization; 3 | 4 | namespace NetDaemon.HassModel.CodeGenerator.Model; 5 | 6 | internal record Response() 7 | { 8 | public bool Optional { get; init; } 9 | } 10 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/Selectors.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | internal record Selector() 6 | { 7 | public bool Multiple { get; init; } 8 | 9 | public string? Type { get; init; } 10 | } 11 | 12 | 13 | internal record AreaSelector : Selector; 14 | 15 | internal record DeviceSelector : Selector; 16 | 17 | internal record EntitySelector : Selector 18 | { 19 | [JsonConverter(typeof(StringAsArrayConverter))] 20 | public string[] Domain { get; init; } = []; 21 | } 22 | 23 | internal record NumberSelector : Selector 24 | { 25 | public double? Step { get; init; } 26 | } 27 | 28 | internal record TargetSelector : Selector 29 | { 30 | [JsonConverter(typeof(SingleObjectAsArrayConverter))] 31 | public EntitySelector[] Entity { get; init; } = []; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/SingleObjectAsArrayConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | /// 6 | /// Converts either a single object or an array to an array 7 | /// 8 | class SingleObjectAsArrayConverter : JsonConverter 9 | { 10 | public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | if (reader.TokenType == JsonTokenType.StartObject) 13 | { 14 | return [JsonSerializer.Deserialize(ref reader, options)!]; 15 | } 16 | 17 | return JsonSerializer.Deserialize(ref reader, options); 18 | } 19 | 20 | public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) 21 | => throw new NotSupportedException(); 22 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/SnakeCaseNamingPolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | internal class SnakeCaseNamingPolicy : JsonNamingPolicy 6 | { 7 | public static SnakeCaseNamingPolicy Instance { get; } = new (); 8 | 9 | public override string ConvertName(string name) => ToSnakeCase(name); 10 | 11 | private static string ToSnakeCase(string str) 12 | { 13 | return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLower(CultureInfo.InvariantCulture); 14 | } 15 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsArrayConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace NetDaemon.HassModel.CodeGenerator.Model; 5 | 6 | /// 7 | /// Converts a Json element that can be a string or a string array 8 | /// 9 | class StringAsArrayConverter : JsonConverter 10 | { 11 | public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | if (reader.TokenType == JsonTokenType.String) 14 | { 15 | return [reader.GetString() ?? throw new UnreachableException("Token is expected to be a string")]; 16 | } 17 | 18 | return JsonSerializer.Deserialize(ref reader, options); 19 | } 20 | 21 | public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options) => throw new NotSupportedException(); 22 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/MetaData/ServicesMetaData/StringAsDoubleConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NetDaemon.HassModel.CodeGenerator.Model; 4 | 5 | class StringAsDoubleConverter : JsonConverter 6 | { 7 | public override double? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 8 | { 9 | // Some fields (step) can have a string or a numeric value. If it is a string we will try to parse it to a decimal 10 | return reader.TokenType switch 11 | { 12 | JsonTokenType.Number => reader.GetDouble(), 13 | JsonTokenType.String => double.TryParse(reader.GetString(), out var d) ? d : null, 14 | _ => Skip(ref reader) 15 | }; 16 | } 17 | 18 | static double? Skip(ref Utf8JsonReader reader) 19 | { 20 | reader.Skip(); 21 | return null; 22 | } 23 | 24 | public override void Write(Utf8JsonWriter writer, double? value, JsonSerializerOptions options) => throw new NotSupportedException(); 25 | } 26 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Service": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/_appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomeAssistant": { 3 | "Host": "ENTER YOUR IP TO Development Home Assistant here", 4 | "Port": 8124, 5 | "Ssl": false, 6 | "Token": "ENTER YOUR TOKEN", 7 | "InsecureBypassCertificateErrors": false 8 | }, 9 | "CodeGeneration": { 10 | "Namespace": "HomeAssistantGenerated", 11 | "OutputFile": "HomeAssistantGenerated.cs", 12 | "OutputFolder": "", 13 | "GenerateOneFilePerEntity": false, 14 | "UseAttributeBaseClasses": "true" 15 | } 16 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.CodeGenerator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomeAssistant": { 3 | "Host": "ENTER YOUR IP TO Development Home Assistant here", 4 | "Port": 8124, 5 | "Ssl": false, 6 | "Token": "ENTER YOUR TOKEN" 7 | }, 8 | "CodeGeneration": { 9 | "Namespace": "HomeAssistantGenerated", 10 | "OutputFile": "HomeAssistantGenerated.cs", 11 | "OutputFolder": "", 12 | "GenerateOneFilePerEntity": false 13 | } 14 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.xUnit1030.severity = none 3 | # Warning CA1861 : Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861) 4 | dotnet_diagnostic.CA1861.severity= none 5 | dotnet_diagnostic.CS0618.severity = none 6 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ControllerTest.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.Client.Settings; 2 | using NetDaemon.HassModel.CodeGenerator; 3 | 4 | namespace NetDaemon.HassModel.Tests.CodeGenerator; 5 | 6 | public class ControllerTest 7 | { 8 | [Fact] 9 | public async Task ControllerShouldReturnDefaultValueForMetadata() 10 | { 11 | // ARRANGE 12 | var controller = new Controller(new CodeGenerationSettings(), new HomeAssistantSettings()); 13 | 14 | // ACT 15 | var result = await controller.LoadEntitiesMetaDataAsync(); 16 | 17 | // ASSERT 18 | result.Domains.Count.Should().Be(2); 19 | result.Domains.Single(x => x.Domain == "light").Should().NotBeNull(); 20 | result.Domains.Single(x => x.Domain == "light").Attributes.SingleOrDefault(x => x.JsonName == "brightness").Should().NotBeNull(); 21 | result.Domains.Single(x => x.Domain == "media_player").Should().NotBeNull(); 22 | result.Domains.Single(x => x.Domain == "media_player").Attributes.SingleOrDefault(x => x.JsonName == "media_artist").Should().NotBeNull(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/NetDaemonTestAppAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Tests.CodeGenerator; 2 | 3 | /// 4 | /// Marker attribute for classes from dynamically compiled code in tests that should be created 5 | /// 6 | [AttributeUsage(AttributeTargets.Class)] 7 | public sealed class NetDaemonTestAppAttribute : Attribute; 8 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/TestFiles/RawVersions/RawVersions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/TestFiles/SubstitutedVersions/SubstitutedVersions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 25.6.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/Entities/EntityExtensions.CallServiceWithResponseAsync.Test.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using NetDaemon.HassModel.Entities; 3 | 4 | namespace NetDaemon.HassModel.Tests.Entities; 5 | 6 | public class EntityExtensionsCallServiceWithResponseAsyncTest 7 | { 8 | [Fact] 9 | public async Task CallServiceWithResponseAsyncShouldReturnCorrectData() 10 | { 11 | var haContextMock = new Mock(); 12 | var entity = new Entity(haContextMock.Object, "domain.test_entity"); 13 | 14 | var response = JsonDocument.Parse("{\"test\": \"test\"}").RootElement; 15 | 16 | haContextMock.Setup(t => t.CallServiceWithResponseAsync("domain", "test_service", It.IsAny(), It.IsAny())) 17 | .Returns(Task.FromResult((JsonElement?) response)); 18 | 19 | var result = await entity.CallServiceWithResponseAsync("test_service", new { test = "test" }); 20 | result.Should().NotBeNull(); 21 | result!.Value.GetProperty("test").GetString().Should().Be("test"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System; 4 | global using System.Linq; 5 | global using System.Threading.Tasks; 6 | global using FluentAssertions; 7 | global using Moq; 8 | global using Xunit; -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs: -------------------------------------------------------------------------------- 1 | using NetDaemon.HassModel.Entities; 2 | 3 | namespace NetDaemon.HassModel.Tests; 4 | 5 | public class ServiceTargetTest 6 | { 7 | [Fact] 8 | public void ServiceTargetShouldContainCorrectEntity() 9 | { 10 | var serviceTarget = ServiceTarget.FromEntity("light.kitchen"); 11 | 12 | serviceTarget.EntityIds.Should().BeEquivalentTo("light.kitchen"); 13 | } 14 | 15 | [Fact] 16 | public void ServiceTargetShouldContainCorrectEntities() 17 | { 18 | var serviceTarget = ServiceTarget.FromEntities(["light.kitchen", "light.livingroom"]); 19 | 20 | serviceTarget.EntityIds.Should().BeEquivalentTo("light.kitchen", "light.livingroom"); 21 | } 22 | 23 | [Fact] 24 | public void ServiceTargetShouldContainCorrectEntitiesUsingParams() 25 | { 26 | var serviceTarget = ServiceTarget.FromEntities("light.kitchen", "light.livingroom"); 27 | 28 | serviceTarget.EntityIds.Should().BeEquivalentTo("light.kitchen", "light.livingroom"); 29 | } 30 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/TestHelpers/HassClient/HaContextMock.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Subjects; 2 | using NetDaemon.HassModel.Entities; 3 | 4 | namespace NetDaemon.HassModel.Tests.TestHelpers.HassClient; 5 | 6 | internal sealed class HaContextMock : Mock 7 | { 8 | public HaContextMock() 9 | { 10 | Setup(m => m.StateAllChanges()).Returns(StateAllChangeSubject); 11 | Setup(m => m.Events).Returns(EventsSubject); 12 | } 13 | 14 | public Subject StateAllChangeSubject { get; } = new(); 15 | public Subject EventsSubject { get; } = new(); 16 | } 17 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel.Tests/TestHelpers/HassClient/TestEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using NetDaemon.HassModel.Entities; 3 | 4 | namespace NetDaemon.HassModel.Tests.TestHelpers.HassClient; 5 | 6 | sealed record TestEntity : Entity, TestEntityAttributes> 7 | { 8 | public TestEntity(IHaContext haContext, string entityId) : base(haContext, entityId) { } 9 | } 10 | 11 | public record TestEntityAttributes 12 | { 13 | [JsonPropertyName("name")] public string Name { get; set; } = ""; 14 | } 15 | 16 | public record NumericTestEntity : NumericEntity, TestEntityAttributes> 17 | { 18 | public NumericTestEntity(Entity entity) : base(entity) 19 | { } 20 | 21 | public NumericTestEntity(IHaContext haContext, string entityId) : base(haContext, entityId) 22 | { } 23 | } 24 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Context.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Context 5 | /// 6 | public class Context 7 | { 8 | /// 9 | /// Id 10 | /// 11 | [JsonPropertyName("id")] public string Id { get; set; } = ""; 12 | 13 | /// 14 | /// ParentId 15 | /// 16 | [JsonPropertyName("parent_id")] public string? ParentId { get; set; } 17 | /// 18 | /// The id of the user who is responsible for the connected item. 19 | /// 20 | [JsonPropertyName("user_id")] public string? UserId { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Entities/Core/IEntityCore.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Entities; 2 | 3 | /// 4 | /// Core interface for any entity 5 | /// 6 | public interface IEntityCore 7 | { 8 | /// 9 | /// The IHAContext 10 | /// 11 | public IHaContext HaContext { get; } 12 | 13 | /// 14 | /// Entity id being handled by this entity 15 | /// 16 | public string EntityId { get; } 17 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Entities/DefaultEntityFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Provides a default implementation of IEntityFactory in case no generated factory is registered 5 | /// 6 | internal class DefaultEntityFactory : IEntityFactory 7 | { 8 | public Entity CreateEntity(IHaContext haContext, string entityId) => new(haContext, entityId); 9 | } 10 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Buffers; 3 | global using System.Linq; 4 | global using System.Collections.Generic; 5 | global using System.Diagnostics.CodeAnalysis; 6 | global using System.Globalization; 7 | global using System.Text.Json; 8 | global using System.Text.Json.Serialization; 9 | global using System.Threading.Tasks; 10 | global using System.Reactive.Subjects; 11 | global using System.Reactive.Linq; 12 | global using Microsoft.Extensions.DependencyInjection; 13 | global using Microsoft.Extensions.Logging; 14 | global using NetDaemon.Client; 15 | global using NetDaemon.Client.HomeAssistant.Model; 16 | global using NetDaemon.HassModel.Internal; 17 | global using NetDaemon.HassModel.Entities; 18 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/HaContextExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Extension methods for HaContext 5 | /// 6 | public static class HaContextExtensions 7 | { 8 | /// 9 | /// The observable state stream state change 10 | /// 11 | /// 12 | /// Old state != New state 13 | /// 14 | public static IObservable StateChanges(this IHaContext haContext) 15 | { 16 | ArgumentNullException.ThrowIfNull(haContext, nameof(haContext)); 17 | return haContext.StateAllChanges().StateChangesOnly(); 18 | } 19 | 20 | /// 21 | /// Filters events on their EventType and retrieves their data in a types object 22 | /// 23 | /// The Event stream 24 | /// The event_type to filter on 25 | /// Type to deserialize of the data json element 26 | /// Observable of matching events with deserialized data 27 | public static IObservable> Filter(this IObservable events, string eventType) 28 | where T : class 29 | => events 30 | .Where(e => e.EventType == eventType && e.DataElement != null) 31 | .Select(e => new Event(e)); 32 | } 33 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/ICacheManager.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Allows initialization and refreshing of the HassModel internal caches 5 | /// 6 | public interface ICacheManager 7 | { 8 | /// 9 | /// (re) Initializes the Hass Model internal caches from Home Assistant. Should be called 10 | /// 11 | /// 12 | /// 13 | Task InitializeAsync(CancellationToken cancellationToken); 14 | } 15 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/IEntityFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Interface for creating Entities based on a HaContext and an EntityId 5 | /// 6 | /// 7 | /// The Code Generator will generate a class that implements this interface to create entities of the apropriate generated types 8 | /// 9 | public interface IEntityFactory 10 | { 11 | /// 12 | /// Creates a (derived) Entity from a haContext and EntityId. To be implemented by the code generator 13 | /// 14 | /// 15 | /// 16 | /// 17 | public Entity CreateEntity(IHaContext haContext, string entityId); 18 | } 19 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/IHaRegistryNavigator.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | internal interface IHaRegistryNavigator : IHaRegistry 4 | { 5 | IEnumerable GetDevicesForArea(Area area); 6 | IEnumerable GetEntitiesForArea(Area area); 7 | IEnumerable GetEntitiesForDevice(Device device); 8 | IEnumerable GetEntitiesForLabel(Label label); 9 | IEnumerable GetAreasForFloor(Floor floor); 10 | } 11 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/ITriggerManager.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel; 2 | 3 | /// 4 | /// Enables the creation of triggers 5 | /// 6 | public interface ITriggerManager 7 | { 8 | /// 9 | /// Registers a trigger in HA and returns an Observable with the events 10 | /// 11 | /// Input data for HA register_trigger command 12 | /// IObservable with all events resulting from this trigger 13 | IObservable RegisterTrigger(object triggerParams); 14 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Internal/CacheManager.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Internal; 2 | 3 | internal class CacheManager(EntityStateCache entityStateCache, RegistryCache registryCache) 4 | : ICacheManager 5 | { 6 | public async Task InitializeAsync(CancellationToken cancellationToken) 7 | { 8 | await entityStateCache.InitializeAsync(cancellationToken).ConfigureAwait(false); 9 | await registryCache.InitializeAsync(cancellationToken).ConfigureAwait(false); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Internal/FormatHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Internal; 2 | 3 | internal class FormatHelpers 4 | { 5 | public static double? ParseAsDouble(string? value) => 6 | double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, NumberFormatInfo.InvariantInfo, out var result) ? result : null; 7 | } 8 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Internal/IBackgroundTaskTracker.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Internal; 2 | 3 | internal interface IBackgroundTaskTracker : IAsyncDisposable 4 | { 5 | /// 6 | /// Tracks a background task and logs exceptions 7 | /// 8 | public void TrackBackgroundTask(Task? task, string? description = null); 9 | } 10 | -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Internal/ScopedObservable.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Internal; 2 | 3 | /// 4 | /// Wraps an Observable so all subscribers can be unsubscribed by disposing 5 | /// 6 | internal sealed class ScopedObservable : IObservable, IDisposable 7 | { 8 | private readonly IDisposable _subscription; 9 | private readonly Subject _subject = new(); 10 | 11 | public ScopedObservable(IObservable innerObservable) 12 | { 13 | _subscription = innerObservable.Subscribe(_subject); 14 | } 15 | 16 | public IDisposable Subscribe(IObserver observer) => _subject.Subscribe(observer); 17 | 18 | public void Dispose() 19 | { 20 | // When disposed unsubscribe from inner observable 21 | // this will make all subscribers of our Subject stop receiving events 22 | _subscription.Dispose(); 23 | _subject.Dispose(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/HassModel/NetDaemon.HassModel/Registry/Area.cs: -------------------------------------------------------------------------------- 1 | namespace NetDaemon.HassModel.Entities; 2 | 3 | /// 4 | /// Details of aa Area in the Home Assistant Registry 5 | /// 6 | public record Area 7 | { 8 | private readonly IHaRegistryNavigator _registry; 9 | 10 | internal Area(IHaRegistryNavigator registry) 11 | { 12 | _registry = registry; 13 | } 14 | /// 15 | /// The area's name 16 | /// 17 | public string? Name { get; init; } 18 | /// 19 | /// The area's Id 20 | /// 21 | public string? Id { get; init; } 22 | 23 | /// 24 | /// The area's Floor 25 | /// 26 | public Floor? Floor { get; init; } 27 | 28 | /// 29 | /// The Devices in this Area 30 | /// 31 | public IReadOnlyCollection Devices => _registry.GetDevicesForArea(this).ToList(); 32 | 33 | /// 34 | /// The Entities in this Area (either direct or via their Device) 35 | /// 36 | public IReadOnlyCollection Entities => _registry.GetEntitiesForArea(this).ToList(); 37 | 38 | /// 39 | /// The Labels of this Area 40 | /// 41 | public IReadOnlyCollection