1
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
using Evergreen.Health.Application.Health.Devices;
|
||||
using Evergreen.Health.Application.Health.Devices.Climate;
|
||||
using Evergreen.Health.Application.Health.Devices.Discovery;
|
||||
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
using Evergreen.Health.Application.Health.Models;
|
||||
using Evergreen.Health.Application.Health.Scenes;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using SystemJson = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace Admin.NET.Test.Health;
|
||||
|
||||
/// <summary>检查 HA JSON 到 API JSON 的完整转换,不连接 HA、不发送家具指令。</summary>
|
||||
public sealed class HomeAssistantAttributeSerializationTests
|
||||
{
|
||||
private const string ClimateId = "climate.test_room";
|
||||
private static HomeAssistantState State(string state = "off") => new()
|
||||
{
|
||||
EntityId = ClimateId,
|
||||
State = state,
|
||||
Attributes = SystemJson.SerializeToElement(new
|
||||
{
|
||||
hvac_modes = new[] { "cool", "heat", "fan_only", "off" },
|
||||
fan_modes = new[] { "自动", "低风", "中风", "高风" },
|
||||
swing_modes = new[] { "off", "vertical" },
|
||||
options = new[] { "智能适配", "轻柔", "全身" },
|
||||
current_temperature = 24.9,
|
||||
temperature = 23,
|
||||
temperature_unit = "°C",
|
||||
min_temp = 5,
|
||||
max_temp = 35,
|
||||
target_temp_step = 0.5,
|
||||
supported_features = 393,
|
||||
fan_mode = "低风"
|
||||
})
|
||||
};
|
||||
|
||||
private static string SerializeApi(object value) => JsonConvert.SerializeObject(value, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
DateFormatString = "yyyy-MM-dd HH:mm:ss"
|
||||
});
|
||||
|
||||
[Theory]
|
||||
[InlineData("hvac_modes")]
|
||||
[InlineData("fan_modes")]
|
||||
[InlineData("swing_modes")]
|
||||
[InlineData("options")]
|
||||
public void ApiSerializesChoiceArraysAsStringsNotJsonElementMetadata(string key)
|
||||
{
|
||||
var state = State();
|
||||
var safe = HomeAssistantDeviceCatalog.ExtractSafeAttributes(state)!;
|
||||
var json = JObject.Parse(SerializeApi(safe));
|
||||
var values = Assert.IsType<JArray>(json[key]);
|
||||
Assert.All(values, value => Assert.Equal(JTokenType.String, value.Type));
|
||||
Assert.Equal(state.Attributes.GetProperty(key).EnumerateArray().Select(value => value.GetString()),
|
||||
values.Select(value => value.Value<string>()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DiscoveredClimateAndParameterReadingsKeepCapabilitiesAcrossApiSerialization(bool useSystemJson)
|
||||
{
|
||||
var state = State();
|
||||
var states = new Dictionary<string, HomeAssistantState> { [ClimateId] = state };
|
||||
var options = Options.Create(new HomeAssistantOptions { Entities = new() { Climate = ClimateId } });
|
||||
var module = new ClimateModule(new DeviceActionRunner(new NoWritesGateway(), TimeProvider.System, new()), options);
|
||||
var catalog = new HomeAssistantDeviceCatalog(options, new DeviceModuleRegistry([module]));
|
||||
var devices = catalog.BuildDiscoveredDevices(new HomeAssistantRegistries
|
||||
{
|
||||
Devices = [new() { Id = "ac", Name = "空调" }],
|
||||
Entities = [new() { EntityId = ClimateId, DeviceId = "ac" }]
|
||||
}, states);
|
||||
var status = new HomeAssistantStatusOutput
|
||||
{
|
||||
Connected = true,
|
||||
Devices = devices,
|
||||
ControlParameters = module.Parameters.ToDictionary(parameter => parameter.Key, parameter =>
|
||||
new HomeAssistantParameterStateOutput
|
||||
{
|
||||
EntityId = parameter.EntityId,
|
||||
State = state.State,
|
||||
Available = true,
|
||||
Attributes = HomeAssistantDeviceCatalog.ExtractSafeAttributes(state)
|
||||
})
|
||||
};
|
||||
var wire = JObject.Parse(useSystemJson
|
||||
? SystemJson.Serialize(status, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
|
||||
: SerializeApi(status));
|
||||
var device = wire["devices"]![0]!;
|
||||
Assert.Equal("climate", device["moduleKey"]!.Value<string>());
|
||||
Assert.True(device["online"]!.Value<bool>());
|
||||
Assert.Equal(ClimateId, device["primaryControlEntityId"]!.Value<string>());
|
||||
var entity = device["entities"]![0]!;
|
||||
Assert.True(entity["available"]!.Value<bool>());
|
||||
Assert.Equal("off", entity["state"]!.Value<string>()); // 关闭不等于不可用。
|
||||
foreach (var reading in new[] { entity, wire["controlParameters"]!["climate_mode"]!,
|
||||
wire["controlParameters"]!["climate_temperature"]!, wire["controlParameters"]!["climate_fan_mode"]! })
|
||||
{
|
||||
var attributes = reading["attributes"]!;
|
||||
Assert.Equal(new[] { "cool", "heat", "fan_only", "off" }, attributes["hvac_modes"]!.Values<string>());
|
||||
Assert.Equal(new[] { "自动", "低风", "中风", "高风" }, attributes["fan_modes"]!.Values<string>());
|
||||
Assert.Equal(24.9, attributes["current_temperature"]!.Value<double>());
|
||||
Assert.Equal(23, attributes["temperature"]!.Value<int>());
|
||||
Assert.Equal("°C", attributes["temperature_unit"]!.Value<string>());
|
||||
Assert.Equal(0.5, attributes["target_temp_step"]!.Value<double>());
|
||||
Assert.Equal(393, attributes["supported_features"]!.Value<int>());
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ConversionPreservesJsonTypesAndDoesNotExposeUnapprovedRootAttributes(bool useSystemJson)
|
||||
{
|
||||
using var document = JsonDocument.Parse("""
|
||||
{"options":["off",null,true,12,0.5,["nested"],{"choice":"cool","flag":false}],
|
||||
"temperature":null,"is_volume_muted":false,"volume_level":0.5,
|
||||
"access_token":"private","unapproved_attribute":{"secret":"private"}}
|
||||
""");
|
||||
var state = new HomeAssistantState { Attributes = document.RootElement };
|
||||
var safe = HomeAssistantDeviceCatalog.ExtractSafeAttributes(state)!;
|
||||
var wire = JObject.Parse(useSystemJson ? SystemJson.Serialize(safe) : SerializeApi(safe));
|
||||
var expected = JObject.Parse("""
|
||||
{"options":["off",null,true,12,0.5,["nested"],{"choice":"cool","flag":false}],
|
||||
"temperature":null,"is_volume_muted":false,"volume_level":0.5}
|
||||
""");
|
||||
Assert.True(JToken.DeepEquals(expected, wire), wire.ToString());
|
||||
}
|
||||
|
||||
private sealed class NoWritesGateway : IHomeAssistantGateway
|
||||
{
|
||||
public Task<IReadOnlyList<HomeAssistantState>> GetStatesAsync(CancellationToken cancellationToken = default)
|
||||
=> throw new InvalidOperationException("Serialization tests must not read a device");
|
||||
public Task CallServiceAsync(string domain, string service, IReadOnlyDictionary<string, object?> data,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> throw new InvalidOperationException("Serialization tests must not control a device");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user