Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added webhook support #369

Merged
merged 1 commit into from
May 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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