Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,8 @@ private static TypeDeclarationSyntax GenerateEntiesForDomainClass(string classNa

private static MemberDeclarationSyntax GenerateEntityProperty(EntityMetaData entity, string className)
{
var entityName = EntityIdHelper.GetEntity(entity.id);

var normalizedPascalCase = entityName.ToNormalizedPascalCase((string)"E_");

var name = entity.friendlyName;
return PropertyWithExpressionBodyNew(className, normalizedPascalCase, "_haContext", $"\"{entity.id}\"").WithSummaryComment(name);
return PropertyWithExpressionBodyNew(className, entity.cSharpName, "_haContext", $"\"{entity.id}\"")
.WithSummaryComment(entity.friendlyName);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ internal record ServiceArgument

public string ParameterTypeName => Required ? TypeName : $"{TypeName}?";

public string PropertyName => HaName.ToNormalizedPascalCase();
public string PropertyName => HaName.ToValidCSharpPascalCase();

public string ParameterName => HaName.ToNormalizedCamelCase();
public string ParameterName => HaName.ToValidCSharpCamelCase();


public string ParameterDefault => Required ? "" : " = null";
}

Expand Down Expand Up @@ -46,15 +45,15 @@ private ServiceArguments(string domain, string serviceName, IReadOnlyCollection<

public IEnumerable<ServiceArgument> Arguments { get; }

public string TypeName => $"{_domain.ToNormalizedPascalCase()}{GetServiceMethodName(_serviceName)}Parameters";
public string TypeName => $"{_domain.ToValidCSharpPascalCase()}{GetServiceMethodName(_serviceName)}Parameters";

public string GetParametersList()
{
var argumentList = Arguments.OrderByDescending(arg => arg.Required);

var anonymousVariableStr = argumentList.Select(x => $"{x.ParameterTypeName} {EscapeIfRequired(x.ParameterName)}{x.ParameterDefault}");

return $"{string.Join(", ", anonymousVariableStr)}";
return string.Join(", ", anonymousVariableStr);
}

public string GetNewServiceArgumentsTypeExpression()
Expand All @@ -71,5 +70,4 @@ private static string EscapeIfRequired(string name)

return match ? "@" + name : name;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ namespace NetDaemon.HassModel.CodeGenerator.Extensions;

internal static class StringExtensions
{
public static string ToNormalizedPascalCase(this string name, string prefix = "HA_")
public static string ToValidCSharpPascalCase(this string name)
{
return name.ToPascalCase().ToNormalized(prefix);
return name.ToPascalCase().ToValidCSharpIdentifier();
}

public static string ToNormalizedCamelCase(this string name, string prefix = "HA_")
public static string ToValidCSharpCamelCase(this string name)
{
return name.ToCamelCase().ToNormalized(prefix);
return name.ToCamelCase().ToValidCSharpIdentifier();
}

private static string ToNormalized(this string name, string prefix = "HA_")
public static string ToValidCSharpIdentifier(this string name)
{
name = name.Replace(".", "_", StringComparison.InvariantCulture);

if (!char.IsLetter(name[0]) && name[0] != '_')
name = prefix + name;
name = Regex.Replace(name, "[^a-zA-Z0-9_]+", "", RegexOptions.Compiled);

return Regex.Replace(name, "[^a-zA-Z0-9]+", "", RegexOptions.Compiled);
if (char.IsAsciiDigit(name[0]))
name = "_" + name;

return name;
}

public static string ToPascalCase(this string str)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,28 @@ internal static class NamingHelper

public static string GetEntitiesForDomainClassName(string prefix)
{
var normalizedDomain = prefix.ToNormalizedPascalCase();
var normalizedDomain = prefix.ToValidCSharpPascalCase();

return $"{normalizedDomain}Entities";
}

public static string GetDomainEntityTypeName(string prefix)
{
var normalizedDomain = prefix.ToNormalizedPascalCase();

return $"{normalizedDomain}Entity";
}

public static string GetServicesTypeName(string prefix)
{
var normalizedDomain = prefix.ToNormalizedPascalCase();
var normalizedDomain = prefix.ToValidCSharpPascalCase();

return $"{normalizedDomain}Services";
}

public static string GetEntityDomainExtensionMethodClassName(string prefix)
{
var normalizedDomain = prefix.ToNormalizedPascalCase();
var normalizedDomain = prefix.ToValidCSharpPascalCase();

return $"{normalizedDomain}EntityExtensionMethods";
}

public static string GetServiceMethodName(string serviceName)
{
serviceName = serviceName.ToNormalizedPascalCase();
serviceName = serviceName.ToValidCSharpPascalCase();

return $"{serviceName}";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static IEnumerable<EntityAttributeMetaData> GetMetaDataFromEntityStates(I
var attributesByJsonName = jsonPropetiesByName
.Select(group => new EntityAttributeMetaData(
JsonName: group.Key,
CSharpName: group.Key.ToNormalizedPascalCase(),
CSharpName: group.Key.ToValidCSharpPascalCase(),
ClrType: GetBestClrType(group.Select(g => g.Value))));

// We ignore possible duplicate CSharp names here, they will be handled later
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ IReadOnlyList<EntityAttributeMetaData> Attributes
private readonly string prefixedDomain = (IsNumeric && EntityIdHelper.MixedDomains.Contains(Domain) ? "numeric_" : "") + Domain;

[JsonIgnore]
public string EntityClassName => GetDomainEntityTypeName(prefixedDomain);
public string EntityClassName => $"{prefixedDomain}Entity".ToValidCSharpPascalCase();

[JsonIgnore]
public string AttributesClassName => $"{prefixedDomain}Attributes".ToNormalizedPascalCase();
public string AttributesClassName => $"{prefixedDomain}Attributes".ToValidCSharpPascalCase();

[JsonIgnore]
public string EntitiesForDomainClassName => $"{Domain}Entities".ToNormalizedPascalCase();
public string EntitiesForDomainClassName => $"{Domain}Entities".ToValidCSharpPascalCase();

[JsonIgnore]
public Type? AttributesBaseClass { get; set; }
};

record EntityMetaData(string id, string? friendlyName);
record EntityMetaData(string id, string? friendlyName, string cSharpName);

record EntityAttributeMetaData(string JsonName, string CSharpName, Type ClrType);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NetDaemon.Client.HomeAssistant.Model;
using System.Diagnostics;
using NetDaemon.Client.HomeAssistant.Model;

namespace NetDaemon.HassModel.CodeGenerator;

Expand All @@ -25,9 +26,40 @@ private static EntityDomainMetadata mapEntityDomainMetadata(IGrouping<(string do
Entities: MapToEntityMetaData(domainGroup),
Attributes: AttributeMetaDataGenerator.GetMetaDataFromEntityStates(domainGroup).ToList());

private static List<EntityMetaData> MapToEntityMetaData(IEnumerable<HassState> g) =>
g.Select(state => new EntityMetaData(state.EntityId, GetFriendlyName(state)))
.OrderBy(s=>s.id).ToList();
private static List<EntityMetaData> MapToEntityMetaData(IEnumerable<HassState> g)
{
var entityMetaDatas = g.Select(state => new EntityMetaData(
id: state.EntityId,
friendlyName: GetFriendlyName(state),
cSharpName: GetPreferredCSharpName(state.EntityId)));

entityMetaDatas = DeDuplicateCSharpNames(entityMetaDatas);

return entityMetaDatas.OrderBy(e => e.id).ToList();
}

private static IEnumerable<EntityMetaData> DeDuplicateCSharpNames(IEnumerable<EntityMetaData> entityMetaDatas)
{
// The PascalCased EntityId might not be unique because we removed all underscores
// If we have duplicates we will use the original ID instead and only make sure it is a Valid C# identifier
return entityMetaDatas
.ToLookup(e => e.cSharpName)
.SelectMany(e => e.Count() == 1
? e
: e.Select(i => i with { cSharpName = GetUniqueCSharpName(i.id) }));
}

/// <summary>
/// We prefer the Property names for Entities to be the id in PascalCase
/// </summary>
private static string GetPreferredCSharpName(string id) => EntityIdHelper.GetEntity(id).ToValidCSharpPascalCase();

/// <summary>
/// HA entity ID's can only contain [a-z0-9_]. Which are all also valid in Csharp identifiers.
/// HA does allow the id to begin with a digit which is not valid for C#. In those cases it will be prefixed with
/// an _
/// </summary>
private static string GetUniqueCSharpName(string id) => EntityIdHelper.GetEntity(id).ToValidCSharpIdentifier();

private static string? GetFriendlyName(HassState hassState) => hassState.AttributesAs<Attributes>()?.friendly_name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,60 @@ public void Run(IHaContext ha)
CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode);
}

[Fact]
public void TestEntityDuplictateNormalizedName()
{
var entityStates = new HassState[]
{
new() { EntityId = "light.light_1_1" },
new() { EntityId = "light.light_11" },
};

var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings, entityStates, Array.Empty<HassServiceDomain>());
var appCode = """
using NetDaemon.HassModel.Entities;
using NetDaemon.HassModel;
using RootNameSpace;

public class Root
{
public void Run(Entities entities)
{
LightEntity l1_1 = entities.Light.light_1_1;
LightEntity l11 = entities.Light.light_11;
}
}
""";
CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode);
}

[Fact]
public void TestEntityInvalidCSharpName()
{
var entityStates = new HassState[]
{
new() { EntityId = "light.1light" },
new() { EntityId = "light.li@#ght" },
};

var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings, entityStates, Array.Empty<HassServiceDomain>());
var appCode = """
using NetDaemon.HassModel.Entities;
using NetDaemon.HassModel;
using RootNameSpace;

public class Root
{
public void Run(Entities entities)
{
LightEntity l1 = entities.Light._1light;
LightEntity l2 = entities.Light.Light;
}
}
""";
CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode);
}

[Fact]
public void TestNumericSensorEntityGeneration()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public void MergeSimple()
{
var previous = new []{new EntityDomainMetadata("light", false, new []
{
new EntityMetaData("light.living", "Livingroom spots"),
new EntityMetaData("light.kitchen", "Kitchen light")
new EntityMetaData("light.living", "Livingroom spots", "Living"),
new EntityMetaData("light.kitchen", "Kitchen light", "Kitchen")
},
new []
{
Expand All @@ -20,20 +20,20 @@ public void MergeSimple()

var current = new []{new EntityDomainMetadata("light", false, new []
{
new EntityMetaData("light.bedroom", "nightlight"),
new EntityMetaData("light.kitchen", "Kitchen light new name")
new EntityMetaData("light.bedroom", "nightlight", "Bedroom"),
new EntityMetaData("light.kitchen", "Kitchen light new name", "Kitchen")
},
new []
{
new EntityAttributeMetaData("off_brightness", "OffBrightness", typeof(double))
})};

var result= EntityMetaDataMerger.Merge(new(), new EntitiesMetaData(){Domains = previous}, new EntitiesMetaData{Domains = current}).Domains;
var result = EntityMetaDataMerger.Merge(new(), new EntitiesMetaData { Domains = previous }, new EntitiesMetaData { Domains = current }).Domains;

var expected = new []{new EntityDomainMetadata("light", false, new []
{
new EntityMetaData("light.bedroom", "nightlight"),
new EntityMetaData("light.kitchen", "Kitchen light new name")
new EntityMetaData("light.bedroom", "nightlight", "Bedroom"),
new EntityMetaData("light.kitchen", "Kitchen light new name", "Kitchen")
},
new []
{
Expand Down