Skip to content

Commit e3a3d96

Browse files
authored
Generate service calls for auto generated entities (#150)
* Generate service calls etc * Fix test
1 parent d9c2ec8 commit e3a3d96

File tree

10 files changed

+2426
-150
lines changed

10 files changed

+2426
-150
lines changed

exampleapps/apps/_EntityExtensions.cs

Lines changed: 106 additions & 101 deletions
Large diffs are not rendered by default.

exampleapps/apps/_EntityExtensionsRx.cs

Lines changed: 2175 additions & 0 deletions
Large diffs are not rendered by default.

exampleapps/apps/test2.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
using System;
66
using System.Reactive.Linq;
77
using System.Collections.Generic;
8-
// using Netdaemon.Generated.Extensions;
9-
public class BatteryManager : NetDaemonRxApp
8+
using Netdaemon.Generated.Reactive;
9+
public class BatteryManager : GeneratedAppBase //NetDaemonRxApp
10+
// public class BatteryManager : NetDaemonRxApp
1011
{
1112
// private ISchedulerResult _schedulerResult;
1213
private int numberOfRuns = 0;
1314

1415
public string? HelloWorldSecret { get; set; }
1516
public override async Task InitializeAsync()
1617
{
18+
// Remote.Tvrummet.TurnOn(new {activity="TV"});
19+
// Log(Remote.Tvrummet.State);
20+
// Log(Remote.Tvrummet.Area);
21+
1722
// SetState("sensor.testing", "on", new { attributeobject = new { aobject = "hello" } });
18-
RunEvery(TimeSpan.FromSeconds(5), () => SetAttribute("Time", DateTime.Now));
19-
Log("Hello");
20-
Log("Hello {name}", "Tomas");
2123
// RunEvery(TimeSpan.FromSeconds(5), () => Log("Hello world!"));
2224
// RunDaily("13:00:00", () => Log("Hello world!"));
2325
// RunIn(TimeSpan.FromSeconds(5), () => Entity("light.tomas_rum").TurnOn());

src/App/NetDaemon.App/Common/ExtensionMethods.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static dynamic ToDynamic(this (string name, object val)[] attributeNameVa
3434
/// </summary>
3535
/// <param name="obj"></param>
3636
/// <returns></returns>
37-
internal static ExpandoObject ToExpandoObject(this object obj)
37+
public static ExpandoObject ToExpandoObject(this object obj)
3838
{
3939
// Null-check
4040

src/App/NetDaemon.App/Common/Reactive/RxEntity.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,14 @@ public interface ISetState
6464
/// </summary>
6565
public class RxEntity : ICanTurnOnAndOff, ISetState, IObserve
6666
{
67-
private readonly INetDaemonReactive _daemonRxApp;
68-
private readonly IEnumerable<string> _entityIds;
67+
/// <summary>
68+
/// The protected daemon app instance
69+
/// </summary>
70+
protected readonly INetDaemonReactive DaemonRxApp;
71+
/// <summary>
72+
/// Entity ids being handled by the RxEntity
73+
/// </summary>
74+
protected readonly IEnumerable<string> EntityIds;
6975

7076
/// <summary>
7177
/// Constructor
@@ -74,16 +80,16 @@ public class RxEntity : ICanTurnOnAndOff, ISetState, IObserve
7480
/// <param name="entityIds">Unique entity id:s</param>
7581
public RxEntity(INetDaemonReactive daemon, IEnumerable<string> entityIds)
7682
{
77-
_daemonRxApp = daemon;
78-
_entityIds = entityIds;
83+
DaemonRxApp = daemon;
84+
EntityIds = entityIds;
7985
}
8086

8187
/// <inheritdoc/>
8288
public IObservable<(EntityState Old, EntityState New)> StateAllChanges
8389
{
8490
get
8591
{
86-
return _daemonRxApp.StateAllChanges.Where(f => _entityIds.Contains(f.New.EntityId));
92+
return DaemonRxApp.StateAllChanges.Where(f => EntityIds.Contains(f.New.EntityId));
8793
}
8894
}
8995

@@ -92,17 +98,17 @@ public RxEntity(INetDaemonReactive daemon, IEnumerable<string> entityIds)
9298
{
9399
get
94100
{
95-
return _daemonRxApp.StateChanges.Where(f => _entityIds.Contains(f.New.EntityId) && f.New?.State != f.Old?.State);
101+
return DaemonRxApp.StateChanges.Where(f => EntityIds.Contains(f.New.EntityId) && f.New?.State != f.Old?.State);
96102
}
97103
}
98104

99105
/// <inheritdoc/>
100106
public void SetState(dynamic state, dynamic? attributes = null)
101107
{
102-
foreach (var entityId in _entityIds)
108+
foreach (var entityId in EntityIds)
103109
{
104110
var domain = GetDomainFromEntity(entityId);
105-
_daemonRxApp.SetState(entityId, state, attributes);
111+
DaemonRxApp.SetState(entityId, state, attributes);
106112
}
107113
}
108114

@@ -131,10 +137,10 @@ internal static string GetDomainFromEntity(string entity)
131137
/// <param name="data">Data to provide</param>
132138
public void CallService(string service, dynamic? data = null)
133139
{
134-
if (_entityIds is null || _entityIds is object && _entityIds.Count() == 0)
140+
if (EntityIds is null || EntityIds is object && EntityIds.Count() == 0)
135141
return;
136142

137-
foreach (var entityId in _entityIds!)
143+
foreach (var entityId in EntityIds!)
138144
{
139145
var serviceData = new FluentExpandoObject();
140146

@@ -154,13 +160,13 @@ public void CallService(string service, dynamic? data = null)
154160

155161
serviceData["entity_id"] = entityId;
156162

157-
_daemonRxApp.CallService(domain, service, serviceData);
163+
DaemonRxApp.CallService(domain, service, serviceData);
158164
}
159165
}
160166

161167
private void CallServiceOnEntity(string service, dynamic? attributes = null)
162168
{
163-
if (_entityIds is null || _entityIds is object && _entityIds.Count() == 0)
169+
if (EntityIds is null || EntityIds is object && EntityIds.Count() == 0)
164170
return;
165171

166172
dynamic? data = null;
@@ -173,7 +179,7 @@ private void CallServiceOnEntity(string service, dynamic? attributes = null)
173179
data = attributes;
174180
}
175181

176-
foreach (var entityId in _entityIds!)
182+
foreach (var entityId in EntityIds!)
177183
{
178184
var serviceData = new FluentExpandoObject();
179185

@@ -187,7 +193,7 @@ private void CallServiceOnEntity(string service, dynamic? attributes = null)
187193

188194
serviceData["entity_id"] = entityId;
189195

190-
_daemonRxApp.CallService(domain, service, serviceData);
196+
DaemonRxApp.CallService(domain, service, serviceData);
191197
}
192198
}
193199
}

src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ public IHttpHandler Http
144144
.AddConsole();
145145
});
146146

147+
148+
public async Task<IEnumerable<HassServiceDomain>> GetAllServices()
149+
{
150+
this._cancelToken.ThrowIfCancellationRequested();
151+
152+
return await _hassClient.GetServices();
153+
}
154+
147155
public void CallService(string domain, string service, dynamic? data = null)
148156
{
149157
this._cancelToken.ThrowIfCancellationRequested();

src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using JoySoftware.HomeAssistant.Client;
12
using JoySoftware.HomeAssistant.NetDaemon.Daemon.Config;
23
using Microsoft.CodeAnalysis;
34
using Microsoft.CodeAnalysis.CSharp;
45
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using System;
57
using System.Collections.Generic;
68
using System.Linq;
79
using System.Runtime.CompilerServices;
@@ -64,6 +66,7 @@ public class CodeGenerator
6466
// Add the classes implementing the specific entities
6567
foreach (var domain in GetDomainsFromEntities(entities))
6668
{
69+
6770
if (_FluentApiMapper.ContainsKey(domain))
6871
{
6972
var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities
@@ -100,11 +103,16 @@ public class CodeGenerator
100103
return code.NormalizeWhitespace(indentation: " ", eol: "\n").ToFullString();
101104
}
102105

103-
public string? GenerateCodeRx(string nameSpace, IEnumerable<string> entities)
106+
public string? GenerateCodeRx(string nameSpace, IEnumerable<string> entities, IEnumerable<HassServiceDomain> services)
104107
{
105108
var code = SyntaxFactory.CompilationUnit();
106109

107110
// Add Usings statements
111+
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")));
112+
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Collections.Generic")));
113+
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Dynamic")));
114+
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq")));
115+
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("JoySoftware.HomeAssistant.NetDaemon.Common")));
108116
code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("JoySoftware.HomeAssistant.NetDaemon.Common.Reactive")));
109117

110118
// Add namespace
@@ -122,22 +130,93 @@ public class CodeGenerator
122130

123131
foreach (var domain in domains)
124132
{
125-
if (_FluentApiMapper.ContainsKey(domain))
133+
var camelCaseDomain = domain.ToCamelCase();
134+
var property = $@"public {camelCaseDomain}Entities {camelCaseDomain} => new {camelCaseDomain}Entities(this);";
135+
var propertyDeclaration = CSharpSyntaxTree.ParseText(property).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
136+
extensionClass = extensionClass.AddMembers(propertyDeclaration);
137+
}
138+
namespaceDeclaration = namespaceDeclaration.AddMembers(extensionClass);
139+
140+
foreach (var domain in GetDomainsFromEntities(entities))
141+
{
142+
var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entity : RxEntity
143+
{{
144+
public string EntityId => EntityIds.First();
145+
146+
public string? Area => DaemonRxApp.State(EntityId)?.Area;
147+
148+
public dynamic? Attribute => DaemonRxApp.State(EntityId)?.Attribute;
149+
150+
public DateTime LastChanged => DaemonRxApp.State(EntityId).LastChanged;
151+
152+
public DateTime LastUpdated => DaemonRxApp.State(EntityId).LastUpdated;
153+
154+
public dynamic? State => DaemonRxApp.State(EntityId)?.State;
155+
156+
public {domain.ToCamelCase()}Entity(INetDaemonReactive daemon, IEnumerable<string> entityIds) : base(daemon, entityIds)
157+
{{
158+
}}
159+
}}";
160+
var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
161+
162+
// var entityIdProperty = $@"public string EntityId => EntityIds.First();";
163+
// var entityIdPropertyDeclaration = CSharpSyntaxTree.ParseText(entityIdProperty).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
164+
// entityClass = entityClass.AddMembers(entityIdPropertyDeclaration);
165+
166+
// var stateProperty = $@"public EntityState? State => DaemonRxApp.State(EntityId)?.State;";
167+
// var statePropertyDeclaration = CSharpSyntaxTree.ParseText(stateProperty).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
168+
// entityClass = entityClass.AddMembers(statePropertyDeclaration);
169+
170+
// They allready have default implementation
171+
var skipServices = new string[] {"turn_on", "turn_off", "toggle"};
172+
173+
foreach (var s in services.Where(n => n.Domain == domain).SelectMany(n => n.Services))
126174
{
127-
var camelCaseDomain = domain.ToCamelCase();
128-
var property = $@"public {camelCaseDomain}Entities {camelCaseDomain} => new {camelCaseDomain}Entities(this);";
129-
var propertyDeclaration = CSharpSyntaxTree.ParseText(property).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
130-
extensionClass = extensionClass.AddMembers(propertyDeclaration);
175+
if (s.Service is null)
176+
continue;
177+
178+
var name = s.Service[(s.Service.IndexOf(".") + 1)..];
179+
180+
if (Array.IndexOf(skipServices, name) >=0)
181+
continue;
182+
183+
// Quick check to make sure the name is a valid C# identifier. Should really check to make
184+
// sure it doesn't collide with a reserved keyword as well.
185+
if (!char.IsLetter(name[0]) && (name[0] != '_'))
186+
{
187+
name = "s_" + name;
188+
}
189+
var hasEntityId = s.Fields.Count(c => c.Field == "entity_id") > 0? true : false;
190+
var entityAssignmentStatement = hasEntityId? @"serviceData[""entity_id""] = EntityId;" : "";
191+
var methodCode = $@"public void {name.ToCamelCase()}(dynamic? data=null)
192+
{{
193+
var serviceData = new FluentExpandoObject();
194+
195+
if (data is ExpandoObject)
196+
{{
197+
serviceData.CopyFrom(data);
198+
}}
199+
else if (data is object)
200+
{{
201+
var expObject = ((object)data).ToExpandoObject();
202+
serviceData.CopyFrom(expObject);
203+
}}
204+
{entityAssignmentStatement}
205+
DaemonRxApp.CallService(""{domain}"", ""{s.Service}"", serviceData);
206+
}}
207+
";
208+
var methodDeclaration = CSharpSyntaxTree.ParseText(methodCode).GetRoot().ChildNodes().OfType<MethodDeclarationSyntax>().FirstOrDefault();
209+
entityClass = entityClass.AddMembers(methodDeclaration);
131210
}
211+
namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass);
212+
132213
}
133-
namespaceDeclaration = namespaceDeclaration.AddMembers(extensionClass);
134214

135215
// Add the classes implementing the specific entities
136216
foreach (var domain in GetDomainsFromEntities(entities))
137217
{
138-
if (_FluentApiMapper.ContainsKey(domain))
139-
{
140-
var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities
218+
219+
var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities
141220
{{
142221
private readonly NetDaemonRxApp _app;
143222
@@ -146,24 +225,23 @@ public class CodeGenerator
146225
_app = app;
147226
}}
148227
}}";
149-
var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
150-
foreach (var entity in entities.Where(n => n.StartsWith(domain)))
228+
var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
229+
foreach (var entity in entities.Where(n => n.StartsWith(domain)))
230+
{
231+
232+
var name = entity[(entity.IndexOf(".") + 1)..];
233+
// Quick check to make sure the name is a valid C# identifier. Should really check to make
234+
// sure it doesn't collide with a reserved keyword as well.
235+
if (!char.IsLetter(name[0]) && (name[0] != '_'))
151236
{
152-
153-
var name = entity[(entity.IndexOf(".") + 1)..];
154-
// Quick check to make sure the name is a valid C# identifier. Should really check to make
155-
// sure it doesn't collide with a reserved keyword as well.
156-
if (!char.IsLetter(name[0]) && (name[0] != '_'))
157-
{
158-
name = "e_" + name;
159-
}
160-
161-
var propertyCode = $@"public RxEntity {name.ToCamelCase()} => _app.Entity(""{entity}"");";
162-
var propDeclaration = CSharpSyntaxTree.ParseText(propertyCode).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
163-
entityClass = entityClass.AddMembers(propDeclaration);
237+
name = "e_" + name;
164238
}
165-
namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass);
239+
240+
var propertyCode = $@"public {domain.ToCamelCase()}Entity {name.ToCamelCase()} => new {domain.ToCamelCase()}Entity(_app, new string[] {{""{entity}""}});";
241+
var propDeclaration = CSharpSyntaxTree.ParseText(propertyCode).GetRoot().ChildNodes().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
242+
entityClass = entityClass.AddMembers(propDeclaration);
166243
}
244+
namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass);
167245
}
168246

169247
code = code.AddMembers(namespaceDeclaration);

src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public static IEnumerable<MetadataReference> GetDefaultReferences()
107107
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
108108
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.DisplayAttribute).Assembly.Location),
109109
MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location),
110+
MetadataReference.CreateFromFile(typeof(System.ComponentModel.INotifyPropertyChanged).Assembly.Location),
110111
MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.DynamicExpression).Assembly.Location),
111112
MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.Logging.Abstractions.NullLogger).Assembly.Location),
112113
MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location),

src/DaemonRunner/DaemonRunner/Service/RunnerService.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
227227
var codeGen = new CodeGenerator();
228228
var source = codeGen.GenerateCode("Netdaemon.Generated.Extensions",
229229
_daemonHost.State.Select(n => n.EntityId).Distinct());
230-
230+
231231
System.IO.File.WriteAllText(System.IO.Path.Combine(sourceFolder!, "_EntityExtensions.cs"), source);
232-
232+
233+
var services = await _daemonHost.GetAllServices();
233234
var sourceRx = codeGen.GenerateCodeRx("Netdaemon.Generated.Reactive",
234-
_daemonHost.State.Select(n => n.EntityId).Distinct());
235+
_daemonHost.State.Select(n => n.EntityId).Distinct(), services);
235236

236237
System.IO.File.WriteAllText(System.IO.Path.Combine(sourceFolder!, "_EntityExtensionsRx.cs"), sourceRx);
237238
}

tests/NetDaemon.Daemon.Tests/Reactive/RxAppTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public async Task NewEventShouldCallFunction()
8787
public async Task NewEventMissingDataAttributeShouldReturnNull()
8888
{
8989
// ARRANGE
90-
var daemonTask = await GetConnectedNetDaemonTask();
90+
var daemonTask = await GetConnectedNetDaemonTask(200);
9191
string? missingAttribute = "has initial value";
9292

9393
// ACT

0 commit comments

Comments
 (0)