Skip to content

Commit

Permalink
Added Entity() and AllEntities() to IHaContext (#474)
Browse files Browse the repository at this point in the history
* Added Entity() and AllEntities() to IHaContext

* add missing files

* Change type if AllEntities

* Fix

* Fix
  • Loading branch information
FrankBakkerNl committed Oct 10, 2021
1 parent 9b9598c commit c4d2d33
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 28 deletions.
2 changes: 2 additions & 0 deletions src/DaemonRunner/DaemonRunner/Service/RunnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken

_loadedDaemonApps = FilterFocusApps(_loadedDaemonApps.ToList());

await Model3.DependencyInjectionSetup.InitializeAsync(_serviceProvider, stoppingToken).ConfigureAwait(false);

IInstanceDaemonApp? codeManager = new CodeManager(_loadedDaemonApps, _logger, _yamlConfig);
await daemonHost.Initialize(codeManager).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading;
using FluentAssertions;
using JoySoftware.HomeAssistant.Client;
using JoySoftware.HomeAssistant.Model;
Expand All @@ -14,8 +16,8 @@ namespace NetDaemon.Model3.Tests.Internal
{
public class AppScopedHaContextProviderTest
{
private Subject<HassEvent> _hassEventSubjectMock = new ();
private readonly Mock<IHassClient> _hassClientMock = new Mock<IHassClient>();
private readonly Subject<HassEvent> _hassEventSubjectMock = new ();
private readonly Mock<IHassClient> _hassClientMock = new();

[Fact]
public void TestCallService()
Expand Down Expand Up @@ -45,8 +47,8 @@ public void TestStateChange()
Data = new HassStateChangedEventData()
{
EntityId = "TestDomain.TestEntity",
NewState = new HassState(){State = "newstate"},
OldState = new HassState(){State = "oldstate"}
NewState = new HassState(){ State = "newstate" },
OldState = new HassState(){ State = "oldstate" }
}
});

Expand All @@ -55,18 +57,22 @@ public void TestStateChange()

haContext.GetState("TestDomain.TestEntity").State!.Should().Be("newstate");
// the state should come from the state cache so we do not expect a call to HassClient.GetState
_hassClientMock.VerifyNoOtherCalls();
_hassClientMock.Verify(m => m.GetState(It.IsAny<string>()), Times.Never);
}


private IHaContext CreateTarget()
{
var serviceCollection = new ServiceCollection();

_hassClientMock.Setup(m => m.GetAllStates(It.IsAny<CancellationToken>())).ReturnsAsync(new List<HassState>());

serviceCollection.AddSingleton(_hassClientMock.Object);
serviceCollection.AddSingleton<IObservable<HassEvent>>(_hassEventSubjectMock);
serviceCollection.AddScopedHaContext();

var provider = serviceCollection.BuildServiceProvider();
DependencyInjectionSetup.InitializeAsync(provider, CancellationToken.None);

var haContext = provider.GetRequiredService<IHaContext>();
return haContext;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Threading;
using JoySoftware.HomeAssistant.Client;
using JoySoftware.HomeAssistant.Model;
using Moq;
Expand All @@ -11,11 +13,15 @@ namespace NetDaemon.Model3.Tests.Internal
public class EntityStateCacheTest
{
[Fact]
public void StateChangeEventIsFirstStoredInCacheThanForwarded()
public async void StateChangeEventIsFirstStoredInCacheThanForwarded()
{
// Arrange
using var testSubject = new Subject<HassEvent>();
using var cache = new EntityStateCache(Mock.Of<IHassClient>(), testSubject);
var hassClientMock = new Mock<IHassClient>();
hassClientMock.Setup(m => m.GetAllStates(CancellationToken.None)).ReturnsAsync(new List<HassState>());

using var cache = new EntityStateCache(hassClientMock.Object, testSubject);
await cache.InitializeAsync(CancellationToken.None);
var handlerCalled = false;

var changedEventData = new HassStateChangedEventData()
Expand Down
15 changes: 15 additions & 0 deletions src/Model3/NetDeamon.Model3/Common/HaContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using NetDaemon.Model3.Entities;

namespace NetDaemon.Model3.Common
{
/// <summary>
/// Extension methods for HaContext
/// </summary>
public static class HaContextExtensions
{
/// <summary>
/// Creates a new Entity instance
/// </summary>
public static Entity Entity(this IHaContext haContext, string entityId) => new (haContext, entityId);
}
}
13 changes: 10 additions & 3 deletions src/Model3/NetDeamon.Model3/Common/IHaContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using NetDaemon.Model3.Entities;

namespace NetDaemon.Model3.Common
Expand All @@ -19,21 +21,26 @@ public interface IHaContext
/// <remarks>
/// Old state != New state
/// </remarks>
IObservable<StateChange> StateChanges { get; }
IObservable<StateChange> StateChanges => StateAllChanges.Where(e => e.New?.State != e.Old?.State);

/// <summary>
/// Get state for a single entity
/// </summary>
/// <param name="entityId"></param>
EntityState? GetState(string entityId);

/// <summary>
/// Gets all the entities in HomeAssistant
/// </summary>
IReadOnlyList<Entity> GetAllEntities();

/// <summary>
/// Calls service in Home Assistant
/// </summary>
/// <param name="domain">Domain of service</param>
/// <param name="service">Service name</param>
/// <param name="target">The target that is targeted by this service call</param>
/// <param name="data">Data provided to service. Use anonomous type</param>
/// <param name="data">Data provided to service. Use anonyomous type</param>
void CallService(string domain, string service, ServiceTarget? target = null, object? data = null);
}
}
}
12 changes: 10 additions & 2 deletions src/Model3/NetDeamon.Model3/DependencyInjectionSetup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Linq;
using JoySoftware.HomeAssistant.Model;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NetDaemon.Model3.Common;
Expand Down Expand Up @@ -31,5 +31,13 @@ internal static void AddScopedHaContext(this IServiceCollection services)
services.AddTransient<IHaContext>(s => s.GetRequiredService<AppScopedHaContextProvider>());
services.AddTransient<IEventProvider>(s => s.GetRequiredService<AppScopedHaContextProvider>());
}

/// <summary>
/// Performs async initialization of the Model3 services
/// </summary>
public static Task InitializeAsync(IServiceProvider sp, CancellationToken cancellationToken)
{
return sp.GetRequiredService<EntityStateCache>().InitializeAsync(cancellationToken);
}
}
}
10 changes: 8 additions & 2 deletions src/Model3/NetDeamon.Model3/Entities/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,17 @@ public Entity(IHaContext haContext, string entityId)
/// <summary>
/// Calls a service using this entity as the target
/// </summary>
/// <param name="service">Name of the service to call</param>
/// <param name="service">Name of the service to call. If the Domain of the service is the same as the domain of the Entity it can be omitted</param>
/// <param name="data">Data to provide</param>
public virtual void CallService(string service, object? data = null)
{
HaContext.CallService(EntityId.SplitEntityId().Domain, service, ServiceTarget.FromEntity(EntityId), data);
if (service == null) throw new ArgumentNullException(nameof(service));

var (serviceDomain, serviceName) = service.SplitAtDot();

serviceDomain ??= EntityId.SplitAtDot().Left ?? throw new InvalidOperationException("EntityId must be formatted 'domain.name'");;

HaContext.CallService(serviceDomain, serviceName, ServiceTarget.FromEntity(EntityId), data);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/Model3/NetDeamon.Model3/Entities/EntityExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ public static class EntityExtensions
/// </summary>
/// <param name="entityState">The state to check</param>
/// <returns>true if the state equals "on", otherwise false</returns>
public static bool IsOn(this EntityState entityState) => string.Equals(entityState?.State, "on", StringComparison.OrdinalIgnoreCase);
public static bool IsOn(this EntityState? entityState) => string.Equals(entityState?.State, "on", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if en EntityState has the state "off"
/// </summary>
/// <param name="entityState">The state to check</param>
/// <returns>true if the state equals "off", otherwise false</returns>
public static bool IsOff(this EntityState entityState) => string.Equals(entityState?.State, "off", StringComparison.OrdinalIgnoreCase);
public static bool IsOff(this EntityState? entityState) => string.Equals(entityState?.State, "off", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if en Entity has the state "on"
/// </summary>
/// <param name="entity">The state to check</param>
/// <returns>true if the state equals "on", otherwise false</returns>
public static bool IsOn(this Entity entity) => entity?.EntityState?.IsOn() ?? false;
public static bool IsOn(this Entity? entity) => entity?.EntityState?.IsOn() ?? false;

/// <summary>
/// Checks if en Entity has the state "off"
/// </summary>
/// <param name="entity">The state to check</param>
/// <returns>true if the state equals "off", otherwise false</returns>
public static bool IsOff(this Entity entity) => entity?.EntityState?.IsOff() ?? false;
public static bool IsOff(this Entity? entity) => entity?.EntityState?.IsOff() ?? false;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using JoySoftware.HomeAssistant.Client;
using JoySoftware.HomeAssistant.Model;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NetDaemon.Infrastructure.ObservableHelpers;
using NetDaemon.Model3.Common;
using NetDaemon.Model3.Entities;
Expand Down Expand Up @@ -40,15 +42,15 @@ internal class AppScopedHaContextProvider : IHaContext, IDisposable, IEventProvi
return HassObjectMapper.Map(hassState);
}

public IReadOnlyList<Entity> GetAllEntities() => _entityStateCache.AllEntityIds.Select(id => new Entity(this, id)).ToList();

public void CallService(string domain, string service, ServiceTarget? target = null, object? data = null)
{
_hassClient.CallService(domain, service, data, target.Map(), false);
}

public IObservable<StateChange> StateAllChanges => _entityStateCache.StateAllChanges.Select(e => e.Map(this));

public IObservable<StateChange> StateChanges => StateAllChanges.Where(e => e.New?.State != e.Old?.State);

public IObservable<T> GetEventDataOfType<T>(string eventType) where T : class =>
_scopedEventObservable
.Where(e => e.EventType == eventType && e.DataElement != null)
Expand All @@ -60,4 +62,4 @@ public void Dispose()
_scopedStateObservable.Dispose();
}
}
}
}
26 changes: 22 additions & 4 deletions src/Model3/NetDeamon.Model3/Internal/EntityStateCache.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.Client;
using JoySoftware.HomeAssistant.Model;
using NetDaemon.Model3.Entities;

namespace NetDaemon.Model3.Internal
{
Expand All @@ -15,31 +20,44 @@ internal class EntityStateCache : IDisposable
private readonly IDisposable _eventSubscription;
private readonly ConcurrentDictionary<string, HassState?> _latestStates = new();

private bool _initialized;

public EntityStateCache(IHassClient hassClient, IObservable<HassEvent> events)
{
_hassClient = hassClient;

_eventSubscription = events.Subscribe(HandleEvent);
}

public async Task InitializeAsync(CancellationToken cancellationToken)
{
var hassStates = await _hassClient.GetAllStates(cancellationToken).ConfigureAwait(false);

foreach (var hassClientState in hassStates)
{
_latestStates[hassClientState.EntityId] = hassClientState;
}
_initialized = true;
}

public IEnumerable<string> AllEntityIds => _latestStates.Select(s => s.Key);

private void HandleEvent(HassEvent hassEvent)
{
if (hassEvent.Data is not HassStateChangedEventData hassStateChangedEventData) return;

// Make sure to first add the new state to the cache before calling other subscribers.
_latestStates[hassStateChangedEventData.EntityId] = hassStateChangedEventData.NewState;

_innerSubject.OnNext(hassStateChangedEventData);
}

public IObservable<HassStateChangedEventData> StateAllChanges => _innerSubject;

public HassState? GetState(string entityId)
{
// Load missing states on demand,
// this is a blocking call if it is not present but we want to avoid making GetState async
if (!_initialized) throw new InvalidOperationException("StateCache has not been initialized yet");

return _latestStates.GetOrAdd(entityId, _ => _hassClient.GetState(entityId).Result);
return _latestStates.TryGetValue(entityId, out var result) ? result : null;
}

public void Dispose()
Expand Down
8 changes: 5 additions & 3 deletions src/Model3/NetDeamon.Model3/Internal/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ public static T ToObject<T>(this JsonElement element, JsonSerializerOptions? opt
return JsonSerializer.Deserialize<T>(bufferWriter.WrittenSpan, options) ?? default!;
}

public static (string Domain, string Entity) SplitEntityId(this string entityId)
public static (string? Left, string Right) SplitAtDot(this string id)
{
var firstDot = entityId.IndexOf('.', System.StringComparison.InvariantCulture);
return (entityId[.. firstDot ], entityId[ firstDot .. ]);
var firstDot = id.IndexOf('.', System.StringComparison.InvariantCulture);
if (firstDot == -1) return (null, id);

return (id[.. firstDot ], id[ firstDot .. ]);
}

}
Expand Down

0 comments on commit c4d2d33

Please sign in to comment.