Skip to content

Commit 1a5ebf3

Browse files
authored
Added support for comples types in config (net-daemon#154)
* Added support for comples types in config * Fix test timing * Test timing * Test fixes
1 parent 5aafedd commit 1a5ebf3

File tree

7 files changed

+960
-839
lines changed

7 files changed

+960
-839
lines changed

exampleapps/apps/_EntityExtensions.cs

+114-112
Large diffs are not rendered by default.

exampleapps/apps/_EntityExtensionsRx.cs

+703-684
Large diffs are not rendered by default.

src/Daemon/NetDaemon.Daemon/Daemon/Config/YamlConfig.cs

+60-40
Original file line numberDiff line numberDiff line change
@@ -92,51 +92,81 @@ public IEnumerable<INetDaemonAppBase> Instances
9292

9393
var valueType = entry.Value.NodeType;
9494

95-
switch (valueType)
96-
{
97-
case YamlNodeType.Sequence:
98-
SetPropertyFromYaml(netDaemonApp, prop, (YamlSequenceNode)entry.Value);
99-
break;
100-
101-
case YamlNodeType.Scalar:
102-
SetPropertyFromYaml(netDaemonApp, prop, (YamlScalarNode)entry.Value);
103-
break;
95+
var instance = InstanceProperty(netDaemonApp, prop.PropertyType, entry.Value);
10496

105-
case YamlNodeType.Mapping:
106-
var map = (YamlMappingNode)entry.Value;
107-
break;
108-
}
97+
prop.SetValue(netDaemonApp, instance);
10998
}
11099

111100
return netDaemonApp;
112101
}
113102

114-
public void SetPropertyFromYaml(INetDaemonAppBase app, PropertyInfo prop, YamlSequenceNode seq)
103+
private object? InstanceProperty(Object? parent, Type instanceType, YamlNode node)
115104
{
116-
if (prop.PropertyType.IsGenericType && prop.PropertyType?.GetGenericTypeDefinition() == typeof(IEnumerable<>))
105+
106+
if (node.NodeType == YamlNodeType.Scalar)
117107
{
118-
Type listType = prop.PropertyType?.GetGenericArguments()[0] ??
119-
throw new NullReferenceException($"The property {prop.Name} of Class {app.GetType().Name} is not compatible with configuration");
108+
var scalarNode = (YamlScalarNode)node;
109+
ReplaceSecretIfExists(scalarNode);
110+
return ((YamlScalarNode)node).ToObject(instanceType);
111+
}
112+
else if (node.NodeType == YamlNodeType.Sequence)
113+
{
114+
if (instanceType.IsGenericType && instanceType?.GetGenericTypeDefinition() == typeof(IEnumerable<>))
115+
{
116+
Type listType = instanceType?.GetGenericArguments()[0] ??
117+
throw new NullReferenceException($"The property {instanceType?.Name} of Class {parent?.GetType().Name} is not compatible with configuration");
120118

121-
IList list = listType.CreateListOfPropertyType() ??
122-
throw new NullReferenceException("Failed to create listtype, plese check {prop.Name} of Class {app.GetType().Name}");
119+
IList list = listType.CreateListOfPropertyType() ??
120+
throw new NullReferenceException("Failed to create listtype, plese check {prop.Name} of Class {app.GetType().Name}");
123121

124-
foreach (YamlNode item in seq.Children)
125-
{
126-
if (item.NodeType != YamlNodeType.Scalar)
122+
foreach (YamlNode item in ((YamlSequenceNode)node).Children)
127123
{
128-
throw new NotSupportedException($"The property {prop.Name} of Class {app.GetType().Name} is not compatible with configuration");
124+
125+
var instance = InstanceProperty(null, listType, item) ??
126+
throw new NotSupportedException($"The class {parent?.GetType().Name} has wrong type in items");
127+
128+
list.Add(instance);
129129
}
130-
var scalarNode = (YamlScalarNode)item;
131-
ReplaceSecretIfExists(scalarNode);
132-
var value = ((YamlScalarNode)item).ToObject(listType) ??
133-
throw new NotSupportedException($"The class {app.GetType().Name} and property {prop.Name} has wrong type in items");
134130

135-
list.Add(value);
131+
return list;
132+
}
133+
}
134+
else if (node.NodeType == YamlNodeType.Mapping)
135+
{
136+
var instance = Activator.CreateInstance(instanceType);
137+
138+
foreach (KeyValuePair<YamlNode, YamlNode> entry in ((YamlMappingNode)node).Children)
139+
{
140+
string? scalarPropertyName = ((YamlScalarNode)entry.Key).Value;
141+
// Just continue to next configuration if null or class declaration
142+
if (scalarPropertyName == null) continue;
143+
144+
var childProp = instanceType.GetYamlProperty(scalarPropertyName) ??
145+
throw new MissingMemberException($"{scalarPropertyName} is missing from the type {instanceType}");
146+
147+
var valueType = entry.Value.NodeType;
148+
Object? result = null;
149+
150+
switch (valueType)
151+
{
152+
case YamlNodeType.Sequence:
153+
result = InstanceProperty(instance, childProp.PropertyType, (YamlSequenceNode)entry.Value);
154+
155+
break;
156+
157+
case YamlNodeType.Scalar:
158+
result = InstanceProperty(instance, childProp.PropertyType, (YamlScalarNode)entry.Value);
159+
break;
160+
161+
case YamlNodeType.Mapping:
162+
var map = (YamlMappingNode)entry.Value;
163+
break;
164+
}
165+
childProp.SetValue(instance, result);
136166
}
137-
// Bind the list to the property
138-
prop.SetValue(app, list);
167+
return instance;
139168
}
169+
return null;
140170
}
141171

142172
private void ReplaceSecretIfExists(YamlScalarNode scalarNode)
@@ -149,16 +179,6 @@ private void ReplaceSecretIfExists(YamlScalarNode scalarNode)
149179
scalarNode.Value = secretReplacement ?? throw new ApplicationException($"{scalarNode.Value!} not found in secrets.yaml");
150180
}
151181

152-
public void SetPropertyFromYaml(INetDaemonAppBase app, PropertyInfo prop, YamlScalarNode sc)
153-
{
154-
ReplaceSecretIfExists(sc);
155-
var scalarValue = sc.ToObject(prop.PropertyType) ??
156-
throw new NotSupportedException($"The class {app.GetType().Name} and property {prop.Name} unexpected value {sc.Value} is wrong type");
157-
158-
// Bind the list to the property
159-
prop.SetValue(app, scalarValue);
160-
}
161-
162182
private string? GetTypeNameFromClassConfig(YamlMappingNode appNode)
163183
{
164184
KeyValuePair<YamlNode, YamlNode> classChild = appNode.Children.Where(n =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using JoySoftware.HomeAssistant.NetDaemon.Common;
5+
/// <summary>
6+
/// Greets (or insults) people when coming home :)
7+
/// </summary>
8+
public class AppComplexConfig : NetDaemonApp
9+
{
10+
public string? AString { get; set; }
11+
public int? AnInt { get; set; }
12+
public bool? ABool { get; set; }
13+
public IEnumerable<string>? AStringList { get; set; }
14+
public IEnumerable<Device>? Devices { get; set; }
15+
public override Task InitializeAsync()
16+
{
17+
// Do nothing
18+
19+
return Task.CompletedTask;
20+
}
21+
}
22+
23+
public class Device
24+
{
25+
public string? name { get; set; }
26+
public IEnumerable<Command>? commands { get; set; }
27+
}
28+
public class Command
29+
{
30+
public string? name { get; set; }
31+
public string? data { get; set; }
32+
}

tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs

+40
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,45 @@ public void YamlScalarNodeToObjectUsingDeciaml()
206206
Assert.Equal((float)1.5f, scalarValue.ToObject(typeof(float)));
207207
Assert.Equal((double)1.5, scalarValue.ToObject(typeof(double)));
208208
}
209+
210+
[Fact]
211+
public void YamlAdvancedObjectsShouldReturnCorrectData()
212+
{
213+
var yaml = @"
214+
a_string: hello world
215+
an_int: 10
216+
a_bool: true
217+
a_string_list:
218+
- hi
219+
- this
220+
- is
221+
- cool!
222+
devices:
223+
- name: tv
224+
commands:
225+
- name: command1
226+
data: some code
227+
- name: command2
228+
data: some code2";
229+
230+
var yamlConfig = new YamlAppConfig(new List<Type>(), new System.IO.StringReader(yaml), new YamlConfig(ConfigFixturePath), ConfigFixturePath);
231+
232+
var yamlStream = new YamlStream();
233+
yamlStream.Load(new StringReader(yaml));
234+
var root = (YamlMappingNode)yamlStream.Documents[0].RootNode;
235+
236+
var instance = (AppComplexConfig?)yamlConfig.InstanceAndSetPropertyConfig(typeof(AppComplexConfig), root, "id");
237+
238+
Assert.Equal("hello world", instance?.AString);
239+
Assert.Equal(10, instance?.AnInt);
240+
Assert.Equal(true, instance?.ABool);
241+
Assert.NotNull(instance?.Devices);
242+
Assert.Equal(1, instance?.Devices.Count());
243+
Assert.Equal("tv", instance?.Devices.First().name);
244+
Assert.Equal("command1", instance?.Devices.First().commands.ElementAt(0).name);
245+
Assert.Equal("some code", instance?.Devices.First().commands.ElementAt(0).data);
246+
Assert.Equal("command2", instance?.Devices.First().commands.ElementAt(1).name);
247+
Assert.Equal("some code2", instance?.Devices.First().commands.ElementAt(1).data);
248+
}
209249
}
210250
}

tests/NetDaemon.Daemon.Tests/Demon/NetDaemonHostTests.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public async Task EventShouldCallCorrectFunction()
2626
{
2727
// ARRANGE
2828

29+
await WaitForDefaultDaemonToConnect(DefaultDaemonHost, CancellationToken.None);
30+
2931
dynamic helloWorldDataObject = GetDynamicDataObject(HelloWorldData);
3032

3133
DefaultHassClientMock.AddCustomEvent("CUSTOM_EVENT", helloWorldDataObject);
@@ -55,7 +57,9 @@ public async Task AttributeServiceCallShouldFindCorrectFunction()
5557
var app = new AssmeblyDaemonApp();
5658
app.Id = "id";
5759

60+
5861
DefaultDaemonHost.InternalRunningAppInstances[app.Id] = app;
62+
await WaitForDefaultDaemonToConnect(DefaultDaemonHost, CancellationToken.None);
5963

6064
// ACT
6165
await app.HandleAttributeInitialization(DefaultDaemonHost).ConfigureAwait(false);
@@ -68,7 +72,7 @@ public async Task AttributeServiceCallShouldFindCorrectFunction()
6872
public void GetStateMissingEntityReturnsNull()
6973
{
7074
// ARRANGE
71-
75+
7276
// ACT
7377
var entity = DefaultDaemonHost.GetState("light.missing_entity");
7478

@@ -203,6 +207,8 @@ public async Task SpeakShouldCallCorrectService()
203207
// ACT
204208
var (daemonTask, _) = ReturnRunningDefauldDaemonHostTask();
205209

210+
await WaitForDefaultDaemonToConnect(DefaultDaemonHost, CancellationToken.None);
211+
206212
DefaultDaemonHost.Speak("media_player.fakeplayer", "Hello test!");
207213

208214
var (_, expObject) = GetDynamicObject(
@@ -226,6 +232,8 @@ public async Task SpeakShouldWaitUntilMediaPlays()
226232
// Get a running default Daemon
227233
var (daemonTask, _) = ReturnRunningDefauldDaemonHostTask(500);
228234

235+
await WaitForDefaultDaemonToConnect(DefaultDaemonHost, CancellationToken.None);
236+
229237
DefaultDaemonHost.InternalDelayTimeForTts = 0; // Allow now extra waittime
230238

231239
// Expected data call service

tests/NetDaemon.Daemon.Tests/Fluent/FluentTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task EntityOnStateChangedForTimeTurnOffLightCallsCorrectServiceCall
3030
var lastChanged = new DateTime(2020, 1, 1, 1, 1, 1, 20);
3131
var lastUpdated = new DateTime(2020, 1, 1, 1, 1, 1, 50);
3232

33-
var daemonTask = RunDefauldDaemonUntilCanceled(200); //overrideDebugNotCancel: true
33+
var daemonTask = RunDefauldDaemonUntilCanceled(300); //overrideDebugNotCancel: true
3434

3535
await WaitForDefaultDaemonToConnect(DefaultDaemonHost, CancellationToken.None);
3636

@@ -48,7 +48,7 @@ public async Task EntityOnStateChangedForTimeTurnOffLightCallsCorrectServiceCall
4848
// ASSERT
4949
await Task.Delay(10); // After 10ms we should not have call
5050
DefaultHassClientMock.VerifyCallServiceTimes("turn_off", Times.Never());
51-
await Task.Delay(200); // After 30ms we should have call
51+
await Task.Delay(300); // After 30ms we should have call
5252
DefaultHassClientMock.VerifyCallServiceTimes("turn_off", Times.Once());
5353
await daemonTask;
5454

0 commit comments

Comments
 (0)