Skip to content

Commit

Permalink
Added webhook support (#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
helto4real committed May 16, 2021
1 parent 7301097 commit c5d8688
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 9 deletions.
8 changes: 8 additions & 0 deletions src/App/NetDaemon.App/Common/INetDaemon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public interface INetDaemon : INetDaemonCommon
/// <param name="waitForResponse">If we should wait for the service to get response from Home Assistant or send/forget scenario</param>
Task CallServiceAsync(string domain, string service, dynamic? data = null, bool waitForResponse = false);

/// <summary>
/// Trigger a state change using trigger templates
/// </summary>
/// <param name="id">webhook id</param>
/// <param name="data">data being sent</param>
/// <param name="waitForResponse">If we should wait for the service to get response from Home Assistant or send/forget scenario</param>
void TriggerWebhook(string id, object? data, bool waitForResponse);

/// <summary>
/// Get application instance by application instance id
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/App/NetDaemon.App/Common/Reactive/INetDaemonReactive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ public interface INetDaemonRxApp : INetDaemonAppBase, IRxSchedule, IRxEntity
/// <param name="script">Script to call</param>
void RunScript(params string[] script);

/// <summary>
/// Trigger a state change using trigger templates
/// </summary>
/// <param name="id">webhook id</param>
/// <param name="data">data being sent</param>
/// <param name="waitForResponse">Waits for Home Assistant to return result before returning</param>
void TriggerWebhook(string id, object? data, bool waitForResponse = false);

/// <summary>
/// Delays timeout time
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/App/NetDaemon.App/Common/Reactive/NetDaemonRxApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ public void RunScript(params string[] script)
Daemon.CallService("script", name);
}
}
/// <inheritdoc/>
public void TriggerWebhook(string id, object? data, bool waitForResponse = false)
{
_ = Daemon ?? throw new NetDaemonNullReferenceException($"{nameof(Daemon)} cant be null!");
Daemon.TriggerWebhook(id, data, waitForResponse);
}

/// <inheritdoc/>
public void SaveData<T>(string id, T data)
Expand Down
2 changes: 1 addition & 1 deletion src/App/NetDaemon.App/NetDaemon.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JoySoftware.HassClient" Version="21.18.2-beta" />
<PackageReference Include="JoySoftware.HassClient" Version="21.19.1-beta" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Roslynator.Analyzers" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
4 changes: 2 additions & 2 deletions src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static class YamlExtensions
return type.GetProperty(propertyName) ?? type.GetProperty(propertyName.ToCamelCase());
}

public static object? ToObject(this YamlScalarNode node, Type valueType, INetDaemonAppBase deamonApp)
public static object? ToObject(this YamlScalarNode node, Type valueType, INetDaemonAppBase? deamonApp)
{
_ = valueType ??
throw new NetDaemonArgumentNullException(nameof(valueType));
Expand Down Expand Up @@ -112,7 +112,7 @@ public static class YamlExtensions
break;
}

if (valueType.IsAssignableTo(typeof(RxEntityBase)))
if (deamonApp != null && valueType.IsAssignableTo(typeof(RxEntityBase)))
{
return Activator.CreateInstance(valueType, deamonApp, new[] {node.Value});
}
Expand Down
63 changes: 61 additions & 2 deletions src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public class NetDaemonHost : INetDaemonHost, IAsyncDisposable
internal readonly Channel<(string, string, dynamic?)> _serviceCallMessageChannel =
Channel.CreateBounded<(string, string, dynamic?)>(200);

internal readonly Channel<(string, object?)> _webhookTriggerMessageChannel =
Channel.CreateBounded<(string, object?)>(200);

internal readonly Channel<(string, dynamic, dynamic?)> _setStateMessageChannel =
Channel.CreateBounded<(string, dynamic, dynamic?)>(200);

Expand Down Expand Up @@ -159,7 +162,26 @@ public async Task<IEnumerable<HassServiceDomain>> GetAllServices()

return await _hassClient.GetServices().ConfigureAwait(false);
}

public void TriggerWebhook(string id, object? data, bool waitForResponse = false)
{
_cancelToken.ThrowIfCancellationRequested();
if (!waitForResponse)
{
if (!_webhookTriggerMessageChannel.Writer.TryWrite((id, data)))
throw new NetDaemonException("Service call queue full!");
}
else
{
try
{
_hassClient.TriggerWebhook(id, data).Wait(_cancelToken);
}
catch (Exception e)
{
Logger.LogError(e, "Failed call service");
}
}
}
public void CallService(string domain, string service, dynamic? data = null, bool waitForResponse = false)
{
_cancelToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -350,9 +372,9 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat
return;
}

// Setup TTS
Task handleTextToSpeechMessagesTask = HandleTextToSpeechMessages(cancellationToken);
Task handleAsyncServiceCalls = HandleAsyncServiceCalls(cancellationToken);
Task handleAsyncWebhookTriggers = HandleAsyncWebhookTriggers(cancellationToken);
Task handleAsyncSetState = HandleAsyncSetState(cancellationToken);

await RefreshInternalStatesAndSetArea().ConfigureAwait(false);
Expand Down Expand Up @@ -1059,6 +1081,10 @@ private void HandleCustomEvent(HassEvent hassEvent, CancellationToken token)
return L;
}
}

// TODO: Refactor the sync to async queue system to allow
// all kinds of types instead of different queues

[SuppressMessage("", "CA1031")]
private async Task HandleAsyncServiceCalls(CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -1093,6 +1119,39 @@ private async Task HandleAsyncServiceCalls(CancellationToken cancellationToken)
}
}

[SuppressMessage("", "CA1031")]
private async Task HandleAsyncWebhookTriggers(CancellationToken cancellationToken)
{
_cancelToken.ThrowIfCancellationRequested();
_ = _hassClient ?? throw new NetDaemonNullReferenceException(nameof(_hassClient));

bool hasLoggedError = false;

while (!cancellationToken.IsCancellationRequested)
{
try
{
(string id, object? data)
= await _webhookTriggerMessageChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);

await _hassClient.TriggerWebhook(id, data).ConfigureAwait(false);

hasLoggedError = false;
}
catch (OperationCanceledException)
{
// Ignore we are leaving
}
catch (Exception e)
{
if (!hasLoggedError)
Logger.LogDebug(e, "Failure sending trigger webhook");
hasLoggedError = true;
await Task.Delay(100, cancellationToken).ConfigureAwait(false); // Do a delay to avoid loop
}
}
}

[SuppressMessage("", "CA1031")]
private async Task HandleAsyncSetState(CancellationToken cancellationToken)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<tags>Home Assistant</tags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JoySoftware.HassClient" Version="21.18.2-beta" />
<PackageReference Include="JoySoftware.HassClient" Version="21.19.1-beta" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="YamlDotNet" Version="11.1.1" />
Expand Down
2 changes: 1 addition & 1 deletion src/DaemonRunner/DaemonRunner/DaemonRunner.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JoySoftware.HassClient" Version="21.18.2-beta" />
<PackageReference Include="JoySoftware.HassClient" Version="21.19.1-beta" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
Expand Down
11 changes: 11 additions & 0 deletions src/Fakes/NetDaemon.Fakes/DaemonHostTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,17 @@ public void VerifyEventSent(string ev, object? eventData)
DefaultHassClientMock.VerifyCallServiceTuple(domain, service, attributesTuples);
}

/// <summary>
/// Verifies that TriggerWebhook is called
/// </summary>
/// <param name="id">Id of webbhook</param>
/// <param name="data">data sent with webhook</param>
/// <param name="times">Number of times called</param>
public void VerifyTriggerWebhook(string id, object? data = null, Times? times = null)
{
DefaultHassClientMock.VerifyTriggerWebhook(id, data, times);
}

/// <summary>
/// Verifies that call_service is called
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Fakes/NetDaemon.Fakes/HassClientMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ public void VerifyCallService(string domain, string service, object? data = null
Verify(n => n.CallService(domain, service, data!, waitForResponse), Times.AtLeastOnce);
}

/// <summary>
/// Verifies that TriggerWebhook is called
/// </summary>
/// <param name="id">Id of webbhook</param>
/// <param name="data">data sent with webhook</param>
/// <param name="times">Number of times called</param>
public void VerifyTriggerWebhook(string id, object? data = null, Times? times = null)
{
if (times is not null)
Verify(n => n.TriggerWebhook(id, data!), times.Value);
else
Verify(n => n.TriggerWebhook(id, data!), Times.AtLeastOnce);
}

/// <summary>
/// Verifies that call_service is called
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,10 @@ public void YamlScalarNotToObjectUsingEntityType(Type entityType)
// ACT & ASSERT
var instance = scalarValue.ToObject(entityType, appMock.Object) as RxEntityBase;
Assert.NotNull(instance);
Assert.Equal(entityType,instance.GetType());
Assert.Equal(entityType,instance!.GetType());
Assert.Equal("1234", instance.EntityId);
}


[Fact]
public void YamlScalarNodeToObjectUsingDecimal()
{
Expand Down
28 changes: 28 additions & 0 deletions tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ public async Task CallServiceShouldCallCorrectFunction()
VerifyCallServiceTuple("mydomain", "myservice", ("attr", "value"));
}

[Fact]
public async Task TriggerWebhookShouldCallCorrectFunction()
{
// ARRANGE
await InitializeFakeDaemon().ConfigureAwait(false);

// ACT
DefaultDaemonRxApp.TriggerWebhook("some-id", new {data = 1});
await RunFakeDaemonUntilTimeout().ConfigureAwait(false);

// ASSERT
VerifyTriggerWebhook("some-id", new {data = 1});
}

[Fact]
public async Task TriggerWebhookWithNullDataShouldCallCorrectFunction()
{
// ARRANGE
await InitializeFakeDaemon().ConfigureAwait(false);

// ACT
DefaultDaemonRxApp.TriggerWebhook("some-id", null);
await RunFakeDaemonUntilTimeout().ConfigureAwait(false);

// ASSERT
VerifyTriggerWebhook("some-id", null);
}

[Fact]
public async Task NewAllEventDataShouldCallFunction()
{
Expand Down

0 comments on commit c5d8688

Please sign in to comment.