Skip to content

Commit

Permalink
Alfa version of Registry objects (#1077)
Browse files Browse the repository at this point in the history
* Draft of Registry object, work in progress

* Before rebase

* Add floors and labels

* Seems complete

* fix some warnings

* Add some tests

* Add Xml comments

* Fix tests

* Increase test coverage

* Clean debug code
  • Loading branch information
FrankBakkerNl authored Apr 28, 2024
1 parent 6bc62b3 commit 917adf5
Show file tree
Hide file tree
Showing 29 changed files with 736 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
public record HassArea
{
[JsonPropertyName("name")] public string? Name { get; init; }

[JsonPropertyName("area_id")] public string? Id { get; init; }
}

[JsonPropertyName("labels")] public IReadOnlyList<string> Labels { get; init; } = Array.Empty<string>();

[JsonPropertyName("floor_id")] public string? FloorId { get; init; }

[JsonPropertyName("icon")] public string? Icon { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ public record HassDevice
[JsonPropertyName("name")] public string? Name { get; init; }

[JsonPropertyName("name_by_user")] public string? NameByUser { get; init; }
}

[JsonPropertyName("labels")] public IReadOnlyList<string> Labels { get; init; } = Array.Empty<string>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public record HassEntity
[JsonPropertyName("icon")] public string? Icon { get; init; }

[JsonPropertyName("platform")] public string? Platform { get; init; }
}

[JsonPropertyName("labels")] public IReadOnlyList<string> Labels { get; init; } = Array.Empty<string>();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NetDaemon.Client.HomeAssistant.Model;
namespace NetDaemon.Client.HomeAssistant.Model;

public record HassLabel
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ public void ShouldWrapAreaFromContext()
{
// Arrange
var haContextMock = new Mock<IHaContext>();
haContextMock.Setup(t => t.GetAreaFromEntityId("domain.testEntity")).Returns(new Area() { Name = "Area Name" });
haContextMock.Setup(t => t.GetEntityRegistration("domain.testEntity")).Returns(new EntityRegistration(){Area = new Area(Mock.Of<IHaRegistryNavigator>()) { Name = "Area Name" }});

// Act
var target = new TestEntity(haContextMock.Object, "domain.testEntity");

// Assert
Assert.Equal("Area Name", target.Area);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Reactive.Subjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NetDaemon.Client;
using NetDaemon.Client.HomeAssistant.Model;
using NetDaemon.Client.Internal.HomeAssistant.Commands;
Expand All @@ -22,7 +24,7 @@ public async Task EntityIdWithArea_Returns_HassArea()
_hassConnectionMock.Setup(n =>
n.SubscribeToHomeAssistantEventsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testSubject);

_hassConnectionMock.Setup(
m => m.SendCommandAndReturnResponseAsync<SimpleCommand, IReadOnlyCollection<HassDevice>>(
It.IsAny<SimpleCommand>(), It.IsAny<CancellationToken>()
Expand Down Expand Up @@ -51,12 +53,12 @@ public async Task EntityIdWithArea_Returns_HassArea()
var serviceColletion = new ServiceCollection();
_ = serviceColletion.AddTransient(_ => new Mock<IObservable<HassEvent>>().Object);

using var cache = new EntityAreaCache(haRunnerMock.Object, serviceColletion.BuildServiceProvider());
using var cache = new RegistryCache(haRunnerMock.Object, NullLogger<RegistryCache>.Instance);

// Act
await cache.InitializeAsync(CancellationToken.None);
// Assert
var area = cache.GetArea("sensor.sensor1");
var area = cache.GetAreaById(cache.GetHassEntityById("sensor.sensor1")!.AreaId);
Assert.NotNull(area);
Assert.Equal("Area Name", area!.Name);
}
Expand All @@ -74,7 +76,7 @@ public async Task EntityIdWithOutArea_ButDeviceArea_Returns_HassArea()
_hassConnectionMock.Setup(n =>
n.SubscribeToHomeAssistantEventsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testSubject);

_hassConnectionMock.Setup(
m => m.SendCommandAndReturnResponseAsync<SimpleCommand, IReadOnlyCollection<HassDevice>>(
It.IsAny<SimpleCommand>(), It.IsAny<CancellationToken>()
Expand Down Expand Up @@ -107,13 +109,13 @@ public async Task EntityIdWithOutArea_ButDeviceArea_Returns_HassArea()
var serviceColletion = new ServiceCollection();
_ = serviceColletion.AddTransient(_ => new Mock<IObservable<HassEvent>>().Object);

using var cache = new EntityAreaCache(haRunnerMock.Object, serviceColletion.BuildServiceProvider());
using var cache = new RegistryCache(haRunnerMock.Object, Mock.Of<ILogger<RegistryCache>>());

// Act
await cache.InitializeAsync(CancellationToken.None);

// Assert
var area = cache.GetArea("sensor.sensor1");
var area = cache.GetAreaById(cache.GetHassEntityById("sensor.sensor1")!.AreaId);
Assert.NotNull(area);
Assert.Equal("Area Name", area!.Name);
}
Expand All @@ -131,7 +133,7 @@ public async Task EntityIdWithArea_AndDeviceArea_Returns_EntityHassArea()
_hassConnectionMock.Setup(n =>
n.SubscribeToHomeAssistantEventsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testSubject);

_hassConnectionMock.Setup(
m => m.SendCommandAndReturnResponseAsync<SimpleCommand, IReadOnlyCollection<HassDevice>>(
It.IsAny<SimpleCommand>(), It.IsAny<CancellationToken>()
Expand Down Expand Up @@ -164,13 +166,13 @@ public async Task EntityIdWithArea_AndDeviceArea_Returns_EntityHassArea()

var serviceColletion = new ServiceCollection();
_ = serviceColletion.AddTransient(_ => new Mock<IObservable<HassEvent>>().Object);
using var cache = new EntityAreaCache(haRunnerMock.Object, serviceColletion.BuildServiceProvider());
using var cache = new RegistryCache(haRunnerMock.Object, Mock.Of<ILogger<RegistryCache>>());

// Act
await cache.InitializeAsync(CancellationToken.None);

// Assert
var area = cache.GetArea("sensor.sensor1");
var area = cache.GetAreaById(cache.GetHassEntityById("sensor.sensor1")!.AreaId);
Assert.NotNull(area);
Assert.Equal("Area2 Name", area!.Name);
}
Expand All @@ -193,7 +195,7 @@ public async Task EntityArea_Updates()
_hassConnectionMock.Setup(n =>
n.SubscribeToHomeAssistantEventsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(testSubject);

_hassConnectionMock.Setup(
m => m.SendCommandAndReturnResponseAsync<SimpleCommand, IReadOnlyCollection<HassArea>>
(
Expand All @@ -219,7 +221,7 @@ public async Task EntityArea_Updates()
_ = serviceColletion.AddTransient<IObservable<HassEvent>>(_ => testSubject);
var sp = serviceColletion.BuildServiceProvider();

using var cache = new EntityAreaCache(haRunnerMock.Object, serviceColletion.BuildServiceProvider());
using var cache = new RegistryCache(haRunnerMock.Object, Mock.Of<ILogger<RegistryCache>>());

// Act 1: Init
await cache.InitializeAsync(CancellationToken.None);
Expand All @@ -236,11 +238,11 @@ public async Task EntityArea_Updates()
});

// Act 3: now fire a area registry update
testSubject.OnNext(new HassEvent { EventType = "area_registry_updated" });
testSubject.OnNext(new HassEvent { EventType = "entity_registry_updated" });

// Assert
var area = cache.GetArea("sensor.sensor1");
var area = cache.GetAreaById(cache.GetHassEntityById("sensor.sensor1")!.AreaId);
Assert.NotNull(area);
Assert.Equal("Area2 Name", area!.Name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Reactive.Subjects;
using Microsoft.Extensions.Logging.Abstractions;
using NetDaemon.Client;
using NetDaemon.Client.HomeAssistant.Model;
using NetDaemon.Client.Internal.HomeAssistant.Commands;
using NetDaemon.HassModel.Internal;

namespace NetDaemon.HassModel.Tests.Registry;

public class RegistryNavigationTest
{
private readonly Mock<IHomeAssistantConnection> _connectionMock;

public RegistryNavigationTest()
{
_connectionMock = new Mock<IHomeAssistantConnection>();
_connectionMock.Setup(m => m.SubscribeToHomeAssistantEventsAsync(null, It.IsAny<CancellationToken>())).ReturnsAsync(new Subject<HassEvent>());
}

private async Task<HaRegistry> InitializeCacheAndBuildRegistry()
{
var runnerMock = new Mock<IHomeAssistantRunner>();
runnerMock.SetupGet(m => m.CurrentConnection).Returns(_connectionMock.Object);

var cache = new RegistryCache(runnerMock.Object, new NullLogger<RegistryCache>());
await cache.InitializeAsync(CancellationToken.None);

var haContextMock = new Mock<IHaContext>();
return new HaRegistry(haContextMock.Object, cache);
}

[Fact]
public async Task TestNavigateModel()
{
SetupCommandResult("config/entity_registry/list",
[
new HassEntity { EntityId = "light.mb_nightlight", AreaId = "master_bedroom"},
new HassEntity { EntityId = "light.babyroom_nightlight", AreaId = "baby_room", Labels = ["stay_on"] },
new HassEntity { EntityId = "sensor.babyroom_humidity", DeviceId = "24:AB:1B:9A"},
]);

SetupCommandResult("config/device_registry/list",
[
new HassDevice { Id = "24:AB:1B:9A", Name = "SomeSensor" , AreaId = "baby_room"}
]);

SetupCommandResult("config/area_registry/list",
[
new HassArea { Name = "Master Bedroom", Id = "master_bedroom", FloorId = "upstairs", Labels = ["bedroom"] },
new HassArea { Name = "Baby room", Id = "baby_room", FloorId = "upstairs", Labels = ["bedroom"] },

new HassArea { Name = "Study", Id = "study", FloorId = "attic" },
new HassArea { Name = "Storage", Id = "storage", FloorId = "attic" },
]);

SetupCommandResult("config/floor_registry/list",
[
new HassFloor { Id = "downstairs", Name = "DownStairs", Level = 0 },
new HassFloor { Id = "upstairs", Name = "Upstairs", Level = 1 },
new HassFloor { Id = "attic", Name = "Attic", Level = 2 },
]
);
SetupCommandResult("config/label_registry/list",
[
new HassLabel { Id = "bedroom", Name = "Bedroom", Description = "Areas that serve as bedrooms" },
new HassLabel { Id = "stay_on", Name = "Stay On", Description = "Lights that should stay on at night" },
]);


// Act:
var registry = await InitializeCacheAndBuildRegistry();

// Assert, navigate the model

registry.Entities.Should().BeEquivalentTo(
[
new { Id = "light.mb_nightlight" },
new { Id = "light.babyroom_nightlight" },
new { Id = "sensor.babyroom_humidity" },
]);

registry.Devices.Should().BeEquivalentTo(
[
new { Name = "SomeSensor" },
]);

registry.Areas.Should().BeEquivalentTo(
[
new { Name = "Master Bedroom" },
new { Name = "Baby room" },
new { Name = "Study" },
new { Name = "Storage" },
]);

registry.Floors.Should().BeEquivalentTo(
[
new { Name = "DownStairs" },
new { Name = "Upstairs" },
new { Name = "Attic" },
]);

registry.Labels.Should().BeEquivalentTo(
[
new { Name = "Bedroom" },
new { Name = "Stay On" },
]);

registry.GetFloor("attic")!.Areas.Should().BeEquivalentTo([
new { Name = "Study" },
new { Name = "Storage" },
]);

registry.GetLabel("stay_on")!.Entities.Should().BeEquivalentTo(
[
new { EntityId = "light.babyroom_nightlight" }
]);


registry.GetLabel("stay_on")!.Entities.Should().Contain(e => e.EntityId == "light.babyroom_nightlight");

registry.GetEntityRegistration("light.mb_nightlight")!.Area!.Name.Should().Be("Master Bedroom");

registry.GetDevice("24:AB:1B:9A")!.Entities.Should().BeEquivalentTo(
[
new { EntityId = "sensor.babyroom_humidity" }
]);

var area = registry.GetArea("baby_room")!;

area.Devices.Should().BeEquivalentTo([
new { Name = "SomeSensor" }
]);
area.Entities.Should().BeEquivalentTo([
new { EntityId = "light.babyroom_nightlight" },
new { EntityId = "sensor.babyroom_humidity" },
]);

}

private void SetupCommandResult<TResult>(string command, IReadOnlyCollection<TResult> result)
{
_connectionMock.Setup(m => m.SendCommandAndReturnResponseAsync<SimpleCommand, IReadOnlyCollection<TResult>>(
new SimpleCommand(command), It.IsAny<CancellationToken>())).ReturnsAsync(result);
}
}
10 changes: 7 additions & 3 deletions src/HassModel/NetDeamon.HassModel/DependencyInjectionSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace NetDaemon.HassModel;
public static class DependencyInjectionSetup
{
/// <summary>
/// Registers services for using the IHaContext interface scoped to NetDeamonApps
/// Registers services for using the IHaContext interface scoped to NetDaemonApps
/// </summary>
public static IHostBuilder UseAppScopedHaContext(this IHostBuilder hostBuilder)
{
Expand All @@ -21,13 +21,17 @@ public static IHostBuilder UseAppScopedHaContext(this IHostBuilder hostBuilder)
}

/// <summary>
/// Registers services for using the IHaContext interface scoped to NetDeamonApps
/// Registers services for using the IHaContext interface scoped to NetDaemonApps
/// </summary>
public static void AddScopedHaContext(this IServiceCollection services)
{
services.AddSingleton<EntityStateCache>();
services.AddSingleton<EntityAreaCache>();
services.AddSingleton<RegistryCache>();
services.AddScoped<HaRegistry>();

services.AddScoped<AppScopedHaContextProvider>();
services.AddScoped<IHaRegistry>(sp => sp.GetRequiredService<AppScopedHaContextProvider>().Registry);
services.AddScoped<IHaRegistryNavigator>(sp => sp.GetRequiredService<AppScopedHaContextProvider>().Registry);
services.AddScoped<BackgroundTaskTracker>();
services.AddScoped<IBackgroundTaskTracker>(s => s.GetRequiredService<BackgroundTaskTracker>());
services.AddTransient<ICacheManager, CacheManager>();
Expand Down
12 changes: 0 additions & 12 deletions src/HassModel/NetDeamon.HassModel/Entities/Area.cs

This file was deleted.

Loading

0 comments on commit 917adf5

Please sign in to comment.