11111
This commit is contained in:
@@ -288,17 +288,27 @@ public sealed class SceneLightingIntegrationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProductionRegistrationAddsConfiguredStripWithoutCreatingASecondDeviceControlImplementation()
|
public async Task ProductionRegistrationAddsSceneStripWithIndependentManualPower()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection(); services.AddOptions(); services.AddLogging(); services.AddHttpClient();
|
var services = new ServiceCollection(); services.AddOptions(); services.AddLogging(); services.AddHttpClient();
|
||||||
services.AddSingleton<IOptions<HomeAssistantOptions>>(Options.Create(Settings()));
|
var settings = Settings();
|
||||||
services.AddHealthDevices(); services.AddSingleton<IHomeAssistantGateway>(new FakeHaGateway());
|
settings.AdditionalSleepLights.Single().StartSleepOnManualPowerOn = false;
|
||||||
|
var gateway = new FakeHaGateway();
|
||||||
|
gateway.States[Strip] = Light(Strip);
|
||||||
|
services.AddSingleton<IOptions<HomeAssistantOptions>>(Options.Create(settings));
|
||||||
|
services.AddHealthDevices(); services.AddSingleton<IHomeAssistantGateway>(gateway);
|
||||||
services.AddSingleton<IAudioPlaybackGateway>(new FakeAudioGateway());
|
services.AddSingleton<IAudioPlaybackGateway>(new FakeAudioGateway());
|
||||||
using var provider = services.BuildServiceProvider();
|
using var provider = services.BuildServiceProvider();
|
||||||
var registry = provider.GetRequiredService<DeviceModuleRegistry>();
|
var registry = provider.GetRequiredService<DeviceModuleRegistry>();
|
||||||
Assert.IsType<ConfiguredSleepLightModule>(registry.ForEntities([Strip]));
|
var strip = Assert.IsType<ConfiguredSleepLightModule>(registry.ForEntities([Strip]));
|
||||||
Assert.IsType<LightingModule>(registry.ForEntities([Primary]));
|
Assert.IsType<LightingModule>(registry.ForEntities([Primary]));
|
||||||
Assert.IsType<ClimateModule>(registry.ForEntities([Climate]));
|
Assert.IsType<ClimateModule>(registry.ForEntities([Climate]));
|
||||||
|
await strip.SetPowerAsync(true, default);
|
||||||
|
var command = Assert.Single(gateway.Calls);
|
||||||
|
Assert.Equal(Strip, command.EntityId);
|
||||||
|
Assert.Equal("turn_on", command.Service);
|
||||||
|
Assert.Single(command.Data!); // 详情页开灯不套用舒睡模式或亮度。
|
||||||
|
Assert.False(provider.GetRequiredService<HomeAssistantLightTransitionService>().ForEntity(Strip).GetStatus().Running);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -96,6 +96,43 @@ public sealed class StandardLightControlTests
|
|||||||
Assert.Single(gateway.Calls);
|
Assert.Single(gateway.Calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConfiguredManualLightsAreDiscoveredIndependentlyWithInventoryFilteringEnabled()
|
||||||
|
{
|
||||||
|
const string auxiliary = "light.auxiliary", unconfigured = "light.unconfigured";
|
||||||
|
var gateway = new FakeHaGateway();
|
||||||
|
foreach (var id in new[] { LightA, LightB, auxiliary, unconfigured }) gateway.States[id] = State(id);
|
||||||
|
var catalog = new HomeAssistantDeviceCatalog(Options.Create(new HomeAssistantOptions
|
||||||
|
{
|
||||||
|
OnlyConfiguredDevices = true, ManualLightEntityIds = [LightA, LightB, LightB]
|
||||||
|
}), new DeviceModuleRegistry([]));
|
||||||
|
var haRegistry = new HomeAssistantRegistries
|
||||||
|
{
|
||||||
|
Devices = [new() { Id = "a", Name = "顶灯一" }, new() { Id = "b", Name = "顶灯二" }, new() { Id = "other" }],
|
||||||
|
Entities = [new() { DeviceId = "a", EntityId = auxiliary }, new() { DeviceId = "a", EntityId = LightA },
|
||||||
|
new() { DeviceId = "b", EntityId = LightB }, new() { DeviceId = "other", EntityId = unconfigured }]
|
||||||
|
};
|
||||||
|
var controller = Controller(gateway);
|
||||||
|
foreach (var registry in new[] { haRegistry, null })
|
||||||
|
{
|
||||||
|
var devices = catalog.BuildDiscoveredDevices(registry, gateway.States);
|
||||||
|
Assert.Equal(2, devices.Count);
|
||||||
|
var first = Assert.Single(devices, item => item.PrimaryControlEntityId == LightA);
|
||||||
|
var second = Assert.Single(devices, item => item.PrimaryControlEntityId == LightB);
|
||||||
|
Assert.NotEqual(first.DeviceId, second.DeviceId);
|
||||||
|
Assert.All(devices, item =>
|
||||||
|
{
|
||||||
|
Assert.Equal(nameof(HealthDeviceCategory.Lighting), item.Category);
|
||||||
|
Assert.Equal("", item.ModuleKey);
|
||||||
|
});
|
||||||
|
await controller.ExecuteAsync(first, gateway.States, "light_brightness", 30, LightA, default);
|
||||||
|
await controller.ExecuteAsync(second, gateway.States, "light_color_temperature", 3100, LightB, default);
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => controller.ExecuteAsync(
|
||||||
|
second, gateway.States, "light_brightness", 40, LightA, default));
|
||||||
|
}
|
||||||
|
Assert.Equal(new[] { LightA, LightB, LightA, LightB }, gateway.Calls.Select(call => call.EntityId));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TwoLightsUseIndependentRangesAndEffectLists()
|
public async Task TwoLightsUseIndependentRangesAndEffectLists()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,6 +35,16 @@
|
|||||||
"Tvoc": "sensor.mirsz_cn_2027039858_et71_tvoc_density_p_3_8"
|
"Tvoc": "sensor.mirsz_cn_2027039858_et71_tvoc_density_p_3_8"
|
||||||
},
|
},
|
||||||
"OnlyConfiguredDevices": true,
|
"OnlyConfiguredDevices": true,
|
||||||
|
"ManualLightEntityIds": [],
|
||||||
|
"AdditionalSleepLights": [
|
||||||
|
{
|
||||||
|
"Key": "roof-light-strip-1",
|
||||||
|
"Name": "超薄智能灯带",
|
||||||
|
"EntityId": "light.hekai1_cn_2224036048_wy0a01_s_2_light",
|
||||||
|
"SleepEffect": "助眠模式",
|
||||||
|
"StartSleepOnManualPowerOn": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"SleepScene": {
|
"SleepScene": {
|
||||||
"TargetHumidity": 55,
|
"TargetHumidity": 55,
|
||||||
"DehumidifierMode": "睡眠",
|
"DehumidifierMode": "睡眠",
|
||||||
|
|||||||
+31
-4
@@ -12,8 +12,14 @@ public sealed class HomeAssistantDeviceCatalog
|
|||||||
{
|
{
|
||||||
private readonly HomeAssistantOptions _options;
|
private readonly HomeAssistantOptions _options;
|
||||||
private readonly DeviceModuleRegistry _modules;
|
private readonly DeviceModuleRegistry _modules;
|
||||||
|
private readonly HashSet<string> _manualLightIds;
|
||||||
public HomeAssistantDeviceCatalog(IOptions<HomeAssistantOptions> options, DeviceModuleRegistry modules)
|
public HomeAssistantDeviceCatalog(IOptions<HomeAssistantOptions> options, DeviceModuleRegistry modules)
|
||||||
=> (_options, _modules) = (options.Value, modules);
|
{
|
||||||
|
(_options, _modules) = (options.Value, modules);
|
||||||
|
_manualLightIds = _options.ManualLightEntityIds
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id) && id.StartsWith("light.", StringComparison.Ordinal))
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
public List<HomeAssistantDiscoveredDeviceOutput> BuildDiscoveredDevices(
|
public List<HomeAssistantDiscoveredDeviceOutput> BuildDiscoveredDevices(
|
||||||
HomeAssistantRegistries? registries,
|
HomeAssistantRegistries? registries,
|
||||||
@@ -42,7 +48,8 @@ public sealed class HomeAssistantDeviceCatalog
|
|||||||
var entityIds = regEntities.Select(entity => entity.EntityId).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var entityIds = regEntities.Select(entity => entity.EntityId).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
var module = _modules.ForEntities(entityIds);
|
var module = _modules.ForEntities(entityIds);
|
||||||
var displayOnly = _options.DisplayOnlyDevices.FirstOrDefault(item => entityIds.Contains(item.EntityId));
|
var displayOnly = _options.DisplayOnlyDevices.FirstOrDefault(item => entityIds.Contains(item.EntityId));
|
||||||
if (_options.OnlyConfiguredDevices && module is null && displayOnly is null) continue;
|
var manualLightId = regEntities.FirstOrDefault(entity => _manualLightIds.Contains(entity.EntityId))?.EntityId;
|
||||||
|
if (_options.OnlyConfiguredDevices && module is null && displayOnly is null && manualLightId is null) continue;
|
||||||
|
|
||||||
var entityOutputs = new List<HomeAssistantEntityOutput>();
|
var entityOutputs = new List<HomeAssistantEntityOutput>();
|
||||||
foreach (var regEntity in regEntities)
|
foreach (var regEntity in regEntities)
|
||||||
@@ -80,9 +87,11 @@ public sealed class HomeAssistantDeviceCatalog
|
|||||||
? an
|
? an
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var category = module?.Category ?? displayOnly?.Category ?? CategorizeDevice(displayName, device.Model ?? "", device.Manufacturer ?? "", entityOutputs);
|
var category = module?.Category ?? (manualLightId is not null ? nameof(HealthDeviceCategory.Lighting) :
|
||||||
|
displayOnly?.Category ?? CategorizeDevice(displayName, device.Model ?? "", device.Manufacturer ?? "", entityOutputs));
|
||||||
var metrics = ExtractDeviceMetrics(category, entityOutputs);
|
var metrics = ExtractDeviceMetrics(category, entityOutputs);
|
||||||
var primaryControlEntityId = module is null && displayOnly is not null ? null : FindPrimaryControlEntityId(category, entityOutputs);
|
var primaryControlEntityId = module is not null ? FindPrimaryControlEntityId(category, entityOutputs) :
|
||||||
|
manualLightId ?? (displayOnly is not null ? null : FindPrimaryControlEntityId(category, entityOutputs));
|
||||||
|
|
||||||
discovered.Add(new HomeAssistantDiscoveredDeviceOutput
|
discovered.Add(new HomeAssistantDiscoveredDeviceOutput
|
||||||
{
|
{
|
||||||
@@ -137,9 +146,27 @@ public sealed class HomeAssistantDeviceCatalog
|
|||||||
Entities = entityOutputs, Metrics = ExtractDeviceMetrics(module.Category, entityOutputs)
|
Entities = entityOutputs, Metrics = ExtractDeviceMetrics(module.Category, entityOutputs)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
foreach (var id in _manualLightIds)
|
||||||
|
{
|
||||||
|
if (_modules.ForEntities([id]) is not null ||
|
||||||
|
_options.ExcludedDeviceEntityIds.Contains(id, StringComparer.OrdinalIgnoreCase) ||
|
||||||
|
!stateMap.TryGetValue(id, out var state)) continue;
|
||||||
|
var name = state.GetStringAttribute("friendly_name") ?? id;
|
||||||
|
var available = IsAvailable(state);
|
||||||
|
list.Add(new()
|
||||||
|
{
|
||||||
|
DeviceId = $"fallback_{id}", Name = name, Category = nameof(HealthDeviceCategory.Lighting), Online = available,
|
||||||
|
PrimaryControlEntityId = state.EntityId, Entities = [new()
|
||||||
|
{
|
||||||
|
EntityId = state.EntityId, Domain = "light", Name = name, State = state.State,
|
||||||
|
Available = available, Attributes = ExtractSafeAttributes(state)
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
foreach (var device in _options.DisplayOnlyDevices)
|
foreach (var device in _options.DisplayOnlyDevices)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(device.EntityId) || _modules.ForEntities([device.EntityId]) is not null ||
|
if (string.IsNullOrWhiteSpace(device.EntityId) || _modules.ForEntities([device.EntityId]) is not null ||
|
||||||
|
_manualLightIds.Contains(device.EntityId) ||
|
||||||
_options.ExcludedDeviceEntityIds.Contains(device.EntityId, StringComparer.OrdinalIgnoreCase)) continue;
|
_options.ExcludedDeviceEntityIds.Contains(device.EntityId, StringComparer.OrdinalIgnoreCase)) continue;
|
||||||
stateMap.TryGetValue(device.EntityId, out var state);
|
stateMap.TryGetValue(device.EntityId, out var state);
|
||||||
var available = state is not null && IsAvailable(state);
|
var available = state is not null && IsAvailable(state);
|
||||||
|
|||||||
@@ -25,8 +25,17 @@ public sealed class ConfiguredSleepLightModule : HealthDeviceModule
|
|||||||
}
|
}
|
||||||
public override async Task SetPowerAsync(bool turnOn, CancellationToken token)
|
public override async Task SetPowerAsync(bool turnOn, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (turnOn) { await _transition.StartSleepAsync(2700, token); return; }
|
if (turnOn && _light.StartSleepOnManualPowerOn)
|
||||||
|
{
|
||||||
|
await _transition.StartSleepAsync(2700, token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await _transition.CancelAsync(token);
|
await _transition.CancelAsync(token);
|
||||||
|
if (turnOn)
|
||||||
|
{
|
||||||
|
await base.SetPowerAsync(true, token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var command = GetShutdownCommands().Single();
|
var command = GetShutdownCommands().Single();
|
||||||
await Runner.SendAsync(command, token);
|
await Runner.SendAsync(command, token);
|
||||||
await Runner.VerifyParameterAsync(command, token);
|
await Runner.VerifyParameterAsync(command, token);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public sealed class HomeAssistantOptions : IConfigurableOptions
|
|||||||
public List<string> ExcludedDeviceEntityIds { get; set; } = [];
|
public List<string> ExcludedDeviceEntityIds { get; set; } = [];
|
||||||
public bool OnlyConfiguredDevices { get; set; }
|
public bool OnlyConfiguredDevices { get; set; }
|
||||||
public List<DisplayOnlyDeviceOptions> DisplayOnlyDevices { get; set; } = [];
|
public List<DisplayOnlyDeviceOptions> DisplayOnlyDevices { get; set; } = [];
|
||||||
|
// 每条独立灯具登记自己的主 light 实体,名称和可控能力读取自 HA。
|
||||||
|
public List<string> ManualLightEntityIds { get; set; } = [];
|
||||||
public HomeAssistantEntityOptions Entities { get; set; } = new();
|
public HomeAssistantEntityOptions Entities { get; set; } = new();
|
||||||
public HomeAssistantSleepSceneOptions SleepScene { get; set; } = new();
|
public HomeAssistantSleepSceneOptions SleepScene { get; set; } = new();
|
||||||
public List<SceneLightOptions> AdditionalSleepLights { get; set; } = [];
|
public List<SceneLightOptions> AdditionalSleepLights { get; set; } = [];
|
||||||
@@ -31,6 +33,7 @@ public sealed class SceneLightOptions
|
|||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string EntityId { get; set; } = string.Empty;
|
public string EntityId { get; set; } = string.Empty;
|
||||||
public string? SleepEffect { get; set; }
|
public string? SleepEffect { get; set; }
|
||||||
|
public bool StartSleepOnManualPowerOn { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class HomeAssistantEntityOptions
|
public sealed class HomeAssistantEntityOptions
|
||||||
|
|||||||
Reference in New Issue
Block a user