149 lines
7.2 KiB
C#
149 lines
7.2 KiB
C#
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[] { "智能适配", "轻柔", "全身" },
|
|
supported_color_modes = new[] { "color_temp" },
|
|
effect_list = 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")]
|
|
[InlineData("supported_color_modes")]
|
|
[InlineData("effect_list")]
|
|
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");
|
|
}
|
|
}
|