Skip to content

Commit

Permalink
Use input booleans instead of switch for app state (#613)
Browse files Browse the repository at this point in the history
* Use helpers for app-state

* Fix warning

* Review comments

* Fix Dispose at the end
  • Loading branch information
helto4real authored Jan 17, 2022
1 parent 80274cb commit a3537f8
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 243 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net;

namespace NetDaemon.Client.Internal.Exceptions;

[SuppressMessage("", "RCS1194")]
public class HomeAssistantApiCallException : ApplicationException
{
public HttpStatusCode Code { get; private set; }
public HomeAssistantApiCallException(string? message, HttpStatusCode code) : base(message)
{
Code = code;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface IHomeAssistantConnection : IHomeAssistantApiManager, IAsyncDisp
Task SendCommandAsync<T>(T command, CancellationToken cancelToken) where T : CommandMessage;

/// <summary>
/// Sends a command message to Home Assistant without handling the result
/// Sends a command message to Home Assistant and return the result
/// </summary>
/// <param name="command">Command message to send</param>
/// <param name="cancelToken">token to cancel operation</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace NetDaemon.Client.Internal.HomeAssistant.Commands;

internal record CreateHelperCommandBase : CommandMessage
{
public CreateHelperCommandBase(string helperType)
{
Type = $"{helperType}/create";
}
[JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
[JsonPropertyName("icon")] public string Icon { get; init; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using NetDaemon.Client.Internal.Exceptions;

namespace NetDaemon.Client.Internal;

internal class HomeAssistantApiManager : IHomeAssistantApiManager
Expand Down Expand Up @@ -39,8 +41,8 @@ HttpClient httpClient
.ConfigureAwait(false);
}

throw new ApplicationException(
$"Call to API unsuccessful, code {result.StatusCode}: reason: {result.ReasonPhrase}");
throw new HomeAssistantApiCallException(
$"Call to API unsuccessful, code {result.StatusCode}: reason: {result.ReasonPhrase}", result.StatusCode);
}

public async Task<T?> PostApiCallAsync<T>(string apiPath, CancellationToken cancelToken, object? data = null)
Expand Down
369 changes: 185 additions & 184 deletions src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs

Large diffs are not rendered by default.

118 changes: 62 additions & 56 deletions src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Reactive.Linq;
using System.Text;
using NetDaemon.AppModel;
using NetDaemon.HassModel.Common;
using NetDaemon.HassModel.Integration;
using NetDaemon.Client.Common.HomeAssistant.Model;
using NetDaemon.Client.Internal.Exceptions;

namespace NetDaemon.Runtime.Internal;

internal class AppStateManager : IAppStateManager, IHandleHomeAssistantAppStateUpdates
internal class AppStateManager : IAppStateManager, IHandleHomeAssistantAppStateUpdates, IDisposable
{
private readonly CancellationTokenSource _cancelTokenSource = new();
private readonly IServiceProvider _provider;
private readonly ConcurrentDictionary<string, ApplicationState> _stateCache = new();

Expand All @@ -25,63 +27,35 @@ public async Task<ApplicationState> GetStateAsync(string applicationId)
{
var entityId = ToSafeHomeAssistantEntityIdFromApplicationId(applicationId);
if (_stateCache.TryGetValue(entityId, out var applicationState)) return applicationState;
// Since IHaContext is scoped and StateManager is singleton we get the
// IHaContext everytime we need to check state
var scope = _provider.CreateScope();
try
{
var haContext = scope.ServiceProvider.GetRequiredService<IHaContext>();

var appState = haContext.GetState(entityId);

if (appState is null)
{
haContext.SetEntityState(entityId, "on");
return ApplicationState.Enabled;
}

var appStateFromHomeAssistant =
appState.State == "on" ? ApplicationState.Enabled : ApplicationState.Disabled;
_stateCache[entityId] = appStateFromHomeAssistant;
return appStateFromHomeAssistant;
}
finally
{
if (scope is IAsyncDisposable serviceScopeAsyncDisposable)
await serviceScopeAsyncDisposable.DisposeAsync().ConfigureAwait(false);
}
return (await GetOrCreateStateForApp(entityId).ConfigureAwait(false))?.State == "on"
? ApplicationState.Enabled
: ApplicationState.Disabled;
}

public async Task SaveStateAsync(string applicationId, ApplicationState state)
{
// Since IHaContext is scoped and StateManager is singleton we get the
// IHaContext everytime we need to check state
var scope = _provider.CreateScope();
try
{
var haContext = scope.ServiceProvider.GetRequiredService<IHaContext>();
var entityId = ToSafeHomeAssistantEntityIdFromApplicationId(applicationId);
var haConnection = _provider.GetRequiredService<IHomeAssistantConnection>() ??
throw new InvalidOperationException();
var entityId = ToSafeHomeAssistantEntityIdFromApplicationId(applicationId);

switch (state)
{
case ApplicationState.Enabled:
haContext.SetEntityState(entityId, "on", new {app_state = "enabled"});
break;
case ApplicationState.Running:
haContext.SetEntityState(entityId, "on", new {app_state = "running"});
break;
case ApplicationState.Error:
haContext.SetEntityState(entityId, "on", new {app_state = "error"});
break;
case ApplicationState.Disabled:
haContext.SetEntityState(entityId, "off", new {app_state = "disabled"});
break;
}
}
finally
_stateCache[entityId] = state;

var currentState = (await GetOrCreateStateForApp(entityId).ConfigureAwait(false))?.State
?? throw new InvalidOperationException();

switch (state)
{
if (scope is IAsyncDisposable serviceScopeAsyncDisposable)
await serviceScopeAsyncDisposable.DisposeAsync().ConfigureAwait(false);
case ApplicationState.Enabled when currentState == "off":
await haConnection.CallServiceAsync("input_boolean", "turn_on",
new HassTarget {EntityIds = new[] {entityId}},
cancelToken: _cancelTokenSource.Token);
break;
case ApplicationState.Disabled when currentState == "on":
await haConnection.CallServiceAsync("input_boolean", "turn_off",
new HassTarget {EntityIds = new[] {entityId}},
cancelToken: _cancelTokenSource.Token);
break;
}
}

Expand Down Expand Up @@ -109,8 +83,6 @@ public void Initialize(IHomeAssistantConnection haConnection, IAppModelContext a
? ApplicationState.Enabled
: ApplicationState.Disabled;

_stateCache[entityId] = appState;

await app.SetStateAsync(
appState
);
Expand Down Expand Up @@ -148,6 +120,40 @@ public static string ToSafeHomeAssistantEntityIdFromApplicationId(string applica
break;
}

return $"switch.netdaemon_{stringBuilder.ToString().ToLowerInvariant()}";
return $"input_boolean.netdaemon_{stringBuilder.ToString().ToLowerInvariant()}";
}

private async Task<HassState?> GetOrCreateStateForApp(string entityId)
{
var haConnection = _provider.GetRequiredService<IHomeAssistantConnection>() ??
throw new InvalidOperationException();
try
{
var state = await haConnection.GetEntityStateAsync(entityId, _cancelTokenSource.Token)
.ConfigureAwait(false);
return state;
}
catch (HomeAssistantApiCallException e)
{
// Missing entity will throw a http status not found
if (e.Code == HttpStatusCode.NotFound)
{
// The app state input_boolean does not exist, lets create a helper
var name = entityId[14..]; // remove the "input_boolean." part
await haConnection.CreateInputBooleanHelperAsync(name, _cancelTokenSource.Token);
_stateCache[entityId] = ApplicationState.Enabled;
await haConnection.CallServiceAsync("input_boolean", "turn_on",
new HassTarget {EntityIds = new[] {entityId}},
cancelToken: _cancelTokenSource.Token).ConfigureAwait(false);
return new HassState {State = "on"};
}

throw;
}
}

public void Dispose()
{
_cancelTokenSource.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using NetDaemon.Runtime.Internal.Model;

namespace NetDaemon.Runtime.Internal;

internal static class HomeAssistantConnectionExtensions
{
public static async Task<InputBooleanHelper?> CreateInputBooleanHelperAsync(
this IHomeAssistantConnection connection,
string name, CancellationToken cancelToken)
{
return await connection.SendCommandAndReturnResponseAsync<CreateInputBooleanHelperCommand, InputBooleanHelper?>(
new CreateInputBooleanHelperCommand
{
Name = name
}, cancelToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using NetDaemon.Client.Common.HomeAssistant.Model;

namespace NetDaemon.Runtime.Internal.Model;

internal record CreateInputBooleanHelperCommand : CommandMessage
{
public CreateInputBooleanHelperCommand()
{
Type = "input_boolean/create";
}

[JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
}
10 changes: 10 additions & 0 deletions src/Runtime/NetDaemon.Runtime/Internal/Model/HassHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace NetDaemon.Runtime.Internal.Model;

internal record InputBooleanHelper
{
[JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
[JsonPropertyName("icon")] public string? Icon { get; init; }
[JsonPropertyName("id")] public string Id { get; init; } = string.Empty;
}

0 comments on commit a3537f8

Please sign in to comment.