Skip to content

Commit 2428f31

Browse files
Merge branch 'main' into minimal_damon
2 parents 59ff124 + abec1da commit 2428f31

File tree

16 files changed

+257
-91
lines changed

16 files changed

+257
-91
lines changed

.github/copilot-instructions.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copilot Instructions for NetDaemon
2+
3+
This document provides guidance for AI assistants working on the NetDaemon project.
4+
5+
## .NET Version
6+
7+
- **Always use the .NET version specified in `global.json`**
8+
9+
## Development Workflow
10+
11+
### Building the Project
12+
```bash
13+
dotnet restore
14+
dotnet build --configuration Release
15+
```
16+
17+
### Running Tests
18+
**Always verify code changes by running tests with `dotnet test`**
19+
20+
The project has comprehensive test coverage including:
21+
- Unit tests for individual components
22+
- Integration tests with Home Assistant (stable and beta versions)
23+
24+
Run specific test projects:
25+
```bash
26+
# Unit tests
27+
dotnet test src/HassModel/NetDaemon.HassModel.Tests
28+
dotnet test src/Extensions/NetDaemon.Extensions.Scheduling.Tests
29+
dotnet test src/Client/NetDaemon.HassClient.Tests
30+
dotnet test src/AppModel/NetDaemon.AppModel.Tests
31+
dotnet test src/Runtime/NetDaemon.Runtime.Tests
32+
33+
# Integration tests
34+
dotnet test tests/Integration/NetDaemon.Tests.Integration
35+
```
36+
37+
### Code Quality
38+
- Follow the existing `.editorconfig` settings
39+
- The project uses Roslynator for additional code analysis
40+
- Build warnings are treated as errors in CI
41+
- Maintain test coverage for new features
42+
43+
### Making Changes
44+
- Focus on minimal, surgical changes
45+
- Maintain backward compatibility where possible
46+
- Update tests for any functional changes
47+
- Consider integration test coverage for Home Assistant interactions
48+
49+
## Key Documentation Links
50+
51+
### NetDaemon Documentation
52+
- **NetDaemon User Docs**: https://netdaemon.xyz/docs/user/
53+
- **NetDaemon Developer Site**: https://netdaemon.xyz/docs/developer
54+
55+
### Home Assistant Documentation
56+
- **Home Assistant Docs**: https://www.home-assistant.io/docs/
57+
- **Home Assistant Developer Docs**: https://developers.home-assistant.io/

.github/workflows/ci_build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ jobs:
7272
if: ${{ github.event_name == 'schedule' && failure() }}
7373
env:
7474
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }}
75-
uses: Ilshidur/action-discord@0.3.2
75+
uses: Ilshidur/action-discord@0.4.0
7676
with:
7777
args: "[Scheduled action failed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})"

global.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.0",
4-
"rollForward": "latestMajor",
5-
"allowPrerelease": true
3+
"version": "9.0.0",
4+
"rollForward": "latestMinor"
65
}
76
}

src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Reflection;
22
using NetDaemon.AppModel.Internal.AppAssemblyProviders;
33
using NetDaemon.AppModel.Internal.AppFactoryProviders;
4-
54
using NetDaemon.AppModel.Internal.Config;
65

76
namespace NetDaemon.AppModel;

src/Extensions/NetDaemon.Extensions.Logging/Common/ServiceCollectionExtensions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.Hosting;
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Hosting;
23
using NetDaemon.Extensions.Logging.Internal;
34
using Serilog;
45

@@ -18,4 +19,15 @@ public static IHostBuilder UseNetDaemonDefaultLogging(this IHostBuilder builder)
1819
return builder.UseSerilog((context, loggerConfiguration) =>
1920
SerilogConfigurator.Configure(loggerConfiguration, context.HostingEnvironment));
2021
}
21-
}
22+
23+
/// <summary>
24+
/// Adds default logging capabilities for NetDaemon
25+
/// </summary>
26+
/// <param name="services"></param>
27+
public static IServiceCollection AddNetDaemonDefaultLogging(this IServiceCollection services)
28+
{
29+
services.AddSerilog((context, loggerConfiguration) =>
30+
SerilogConfigurator.Configure(loggerConfiguration, context.GetRequiredService<IHostEnvironment>()));
31+
return services;
32+
}
33+
}

src/Extensions/NetDaemon.Extensions.MqttEntityManager/DependencyInjectionSetup.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,25 @@ public static class DependencyInjectionSetup
2020
/// <returns></returns>
2121
public static IHostBuilder UseNetDaemonMqttEntityManagement(this IHostBuilder hostBuilder)
2222
{
23-
return hostBuilder.ConfigureServices((context, services) =>
23+
return hostBuilder.ConfigureServices((_, services) =>
2424
{
25-
services.AddSingleton<IMqttFactory, MqttFactoryFactory>();
26-
services.AddSingleton<IMqttClientOptionsFactory, MqttClientOptionsFactory>();
27-
services.AddSingleton<IMqttFactoryWrapper, MqttFactoryWrapper>();
28-
services.AddSingleton<IMqttEntityManager, MqttEntityManager>();
29-
services.AddSingleton<IAssuredMqttConnection, AssuredMqttConnection>();
30-
services.AddSingleton<IMessageSender, MessageSender>();
31-
services.AddSingleton<IMessageSubscriber, MessageSubscriber>();
32-
services.Configure<MqttConfiguration>(context.Configuration.GetSection("Mqtt"));
25+
services.AddNetDaemonMqttEntityManagement();
3326
});
3427
}
35-
}
28+
29+
/// <summary>
30+
/// Add support for managing entities via MQTT
31+
/// </summary>
32+
public static IServiceCollection AddNetDaemonMqttEntityManagement(this IServiceCollection services)
33+
{
34+
services.AddSingleton<IMqttFactory, MqttFactoryFactory>();
35+
services.AddSingleton<IMqttClientOptionsFactory, MqttClientOptionsFactory>();
36+
services.AddSingleton<IMqttFactoryWrapper, MqttFactoryWrapper>();
37+
services.AddSingleton<IMqttEntityManager, MqttEntityManager>();
38+
services.AddSingleton<IAssuredMqttConnection, AssuredMqttConnection>();
39+
services.AddSingleton<IMessageSender, MessageSender>();
40+
services.AddSingleton<IMessageSubscriber, MessageSubscriber>();
41+
services.AddOptions<MqttConfiguration>().BindConfiguration("Mqtt");
42+
return services;
43+
}
44+
}

src/Extensions/NetDaemon.Extensions.Tts/HostBuilderExtensions.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using Microsoft.Extensions.DependencyInjection;
2-
using Microsoft.Extensions.Hosting;
3-
using NetDaemon.Extensions.Tts.Internal;
1+
using Microsoft.Extensions.Hosting;
42

53
namespace NetDaemon.Extensions.Tts;
64

@@ -17,9 +15,8 @@ public static IHostBuilder UseNetDaemonTextToSpeech(this IHostBuilder hostBuilde
1715
{
1816
hostBuilder.ConfigureServices(services =>
1917
{
20-
services.AddSingleton<TextToSpeechService>();
21-
services.AddSingleton<ITextToSpeechService>(s => s.GetRequiredService<TextToSpeechService>());
18+
services.AddNetDaemonTextToSpeech();
2219
});
2320
return hostBuilder;
2421
}
25-
}
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using NetDaemon.Extensions.Tts.Internal;
3+
4+
namespace NetDaemon.Extensions.Tts;
5+
6+
/// <summary>
7+
/// Extension methods for text-to-speech
8+
/// </summary>
9+
public static class ServiceCollectionExtensions
10+
{
11+
/// <summary>
12+
/// Use the text-to-speech engine of NetDaemon
13+
/// </summary>
14+
/// <param name="services"></param>
15+
public static IServiceCollection AddNetDaemonTextToSpeech(this IServiceCollection services)
16+
{
17+
services.AddSingleton<TextToSpeechService>();
18+
services.AddSingleton<ITextToSpeechService>(s => s.GetRequiredService<TextToSpeechService>());
19+
return services;
20+
}
21+
}

src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/HelpersGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ private static MethodDeclarationSyntax BuildAddHomeAssistantGenerated(IEnumerabl
6565
private static IEnumerable<string> GetInjectableTypes(IEnumerable<EntityDomainMetadata> domains, IEnumerable<HassServiceDomain> orderedServiceDomains) =>
6666
[
6767
EntitiesClassName,
68-
.. domains.Select(d => d.EntitiesForDomainClassName),
68+
.. domains.Select(d => d.EntitiesForDomainClassName).Distinct(),
6969
ServicesClassName,
70-
..orderedServiceDomains.Select(d => GetServicesTypeName(d.Domain))
70+
..orderedServiceDomains.Select(d => GetServicesTypeName(d.Domain)).Distinct()
7171
];
7272
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Text.RegularExpressions;
2+
using NetDaemon.Client.HomeAssistant.Model;
3+
using NetDaemon.HassModel.CodeGenerator;
4+
using NetDaemon.HassModel.CodeGenerator.CodeGeneration;
5+
6+
namespace NetDaemon.HassModel.Tests.CodeGenerator;
7+
8+
public class HelpersGeneratorTest
9+
{
10+
[Fact]
11+
public void AddHomeAssistantGenerated_ShouldNotRegisterDuplicateEntityClasses()
12+
{
13+
// Arrange: Create test data that would lead to duplicate SensorEntities registrations
14+
var states = new HassState[]
15+
{
16+
// Non-numeric sensor (no unit_of_measurement)
17+
new() { EntityId = "sensor.simple_text", Attributes = new Dictionary<string, object>() },
18+
19+
// Numeric sensor (has unit_of_measurement)
20+
new() { EntityId = "sensor.temperature", Attributes = new Dictionary<string, object> { ["unit_of_measurement"] = "°C" } }
21+
};
22+
23+
// Act: Generate metadata which creates both numeric and non-numeric sensor domains
24+
var metaData = EntityMetaDataGenerator.GetEntityDomainMetaData(states);
25+
26+
// Both should have the same EntitiesForDomainClassName
27+
var sensorDomains = metaData.Domains.Where(d => d.Domain == "sensor").ToList();
28+
sensorDomains.Should().HaveCount(2, "there should be both numeric and non-numeric sensor domains");
29+
sensorDomains.Should().AllSatisfy(d => d.EntitiesForDomainClassName.Should().Be("SensorEntities"));
30+
31+
// Generate the extension method code
32+
var generatedMembers = HelpersGenerator.Generate(metaData.Domains, []).ToList();
33+
var generatedCode = generatedMembers.First().ToString();
34+
35+
// Assert: The generated code should not contain duplicate SensorEntities registrations
36+
var sensorEntitiesMatches = Regex.Matches(
37+
generatedCode,
38+
@"serviceCollection\.AddTransient<SensorEntities>\(\);");
39+
40+
sensorEntitiesMatches.Should().HaveCount(1, "SensorEntities should only be registered once, not duplicated");
41+
}
42+
43+
[Fact]
44+
public void AddHomeAssistantGenerated_ShouldRegisterAllUniqueEntityClasses()
45+
{
46+
// Arrange: Create test data with different domains
47+
var states = new HassState[]
48+
{
49+
new() { EntityId = "sensor.temperature", Attributes = new Dictionary<string, object> { ["unit_of_measurement"] = "°C" } },
50+
new() { EntityId = "light.living_room", Attributes = new Dictionary<string, object>() },
51+
new() { EntityId = "switch.kitchen", Attributes = new Dictionary<string, object>() }
52+
};
53+
54+
// Act: Generate metadata
55+
var metaData = EntityMetaDataGenerator.GetEntityDomainMetaData(states);
56+
57+
// Generate the extension method code
58+
var generatedMembers = HelpersGenerator.Generate(metaData.Domains, []).ToList();
59+
var generatedCode = generatedMembers.First().ToString();
60+
61+
// Assert: Each domain should be registered exactly once
62+
generatedCode.Should().Contain("serviceCollection.AddTransient<SensorEntities>();");
63+
generatedCode.Should().Contain("serviceCollection.AddTransient<LightEntities>();");
64+
generatedCode.Should().Contain("serviceCollection.AddTransient<SwitchEntities>();");
65+
66+
// Verify no duplicates
67+
var sensorMatches = Regex.Matches(generatedCode, @"serviceCollection\.AddTransient<SensorEntities>\(\);");
68+
var lightMatches = Regex.Matches(generatedCode, @"serviceCollection\.AddTransient<LightEntities>\(\);");
69+
var switchMatches = Regex.Matches(generatedCode, @"serviceCollection\.AddTransient<SwitchEntities>\(\);");
70+
71+
sensorMatches.Should().HaveCount(1);
72+
lightMatches.Should().HaveCount(1);
73+
switchMatches.Should().HaveCount(1);
74+
}
75+
}

0 commit comments

Comments
 (0)