Files
Rbm.FamStack/tests/FamilyPlanner.Web.Tests/House/FakeHomeApi.cs
T
openclaw 70f1890641 feat: Phase 19 Haus - Vallox MV ventilation unit (profile, temperatures, profile switching)
- New device kind Ventilation with driver Vallox (WebSocket protocol of the unit's web UI:
  READ_TABLES/WRITE_DATA, checksum, big-endian table); no migration (enums stored as text)
- Read profile incl. remaining minutes, outdoor/supply/extract/exhaust air temperatures,
  humidity, CO2, fan speed; activate Home/Away/Boost/Fireplace (timed profiles use the
  duration configured on the unit)
- Same SSRF protection as Shelly (shared connection handler), permissions, action log, audit
- Admin dialog without generation/channel/credentials for ventilation, device card with
  values and profile buttons
- Tests: unit (protocol vs. reference bytes, driver, loopback WebSocket, SSRF, rules), bUnit
- ADR-0025, roadmap phase 19
2026-10-16 20:04:14 +02:00

213 lines
9.9 KiB
C#

using FamilyPlanner.Domain;
using FamilyPlanner.Domain.Home;
using FamilyPlanner.Shared;
using FamilyPlanner.Shared.Admin;
using FamilyPlanner.Shared.Home;
using FamilyPlanner.Web.Client.Services;
namespace FamilyPlanner.Web.Tests.House;
internal sealed class FakeHomeApi : IHomeApi
{
public HomeOverviewDto Overview { get; set; } = new([], [], [], false);
public Dictionary<Guid, HomeDeviceStateDto> States { get; } = [];
public List<(Guid Id, HomeCommand Command)> Commands { get; } = [];
/// <summary>Antwort auf Befehle; <c>null</c> = der Standard (Gerät folgt dem Befehl).</summary>
public Func<Guid, HomeCommand, ApiResult<HomeCommandResultDto>>? CommandHandler { get; set; }
public int OverviewCalls { get; private set; }
public int StateCalls { get; private set; }
public ApiProblem? OverviewProblem { get; set; }
// Verwaltung
public List<HomeAdminDeviceDto> Devices { get; } = [];
public List<HomeAdminRoomDto> Rooms { get; } = [];
public List<HomeAdminPlanDto> Plans { get; } = [];
public List<UserDirectoryEntry> Users { get; } = [];
public List<SaveHomeDeviceRequest> SavedDevices { get; } = [];
public List<(Guid Id, SetHomeAccessRequest Request)> SavedAccess { get; } = [];
public List<TestHomeDeviceRequest> DraftTests { get; } = [];
public List<(Guid Id, SaveHomePlacementsRequest Request)> SavedPlacements { get; } = [];
public List<SaveHomeRoomRequest> SavedRooms { get; } = [];
public List<Guid> Deleted { get; } = [];
public HomeTestResultDto TestResult { get; set; } = new(true, "Verbindung erfolgreich.", "SNSW-001X16EU", "1.4.4", 42);
public ApiProblem? SaveProblem { get; set; }
public static HomeDeviceDto Device(string name, HomeDeviceKind kind = HomeDeviceKind.Switch, Guid? room = null, bool canControl = true, string? icon = null) =>
new(Guid.NewGuid(), name, kind, icon, room, canControl);
public static HomeDeviceStateDto On(Guid id, double? watts = null) => new(id, true, true, null, null, watts, null, null, null);
public static HomeDeviceStateDto Off(Guid id) => new(id, true, false, null, null, null, null, null, null);
public static HomeDeviceStateDto Offline(Guid id, string message = "Das Gerät ist nicht erreichbar.") => new(id, false, null, null, null, null, null, null, message);
public static HomeDeviceStateDto Cover(Guid id, string state, int? position = null) => new(id, true, null, state, position, null, null, null, null);
public static HomeDeviceStateDto Sensor(Guid id, double temperature, double humidity) => new(id, true, null, null, null, null, temperature, humidity, null);
public static HomeDeviceStateDto Ventilation(Guid id, HomeVentilationProfile profile, int? minutesLeft = null) =>
new(id, true, null, null, null, null, 21.7, 45, null, new HomeVentilationDto(profile, minutesLeft, 5.2, 18.9, 21.7, 7.1, 40, 620));
public Task<ApiResult<HomeOverviewDto>> GetOverviewAsync(CancellationToken ct = default)
{
OverviewCalls++;
return Task.FromResult(OverviewProblem is { } problem ? ApiResult<HomeOverviewDto>.Fail(problem) : ApiResult<HomeOverviewDto>.Ok(Overview));
}
public Task<ApiResult<HomeStatesDto>> GetStatesAsync(CancellationToken ct = default)
{
StateCalls++;
return Task.FromResult(ApiResult<HomeStatesDto>.Ok(new HomeStatesDto([.. States.Values], DateTimeOffset.UtcNow)));
}
public Task<ApiResult<HomeCommandResultDto>> SendCommandAsync(Guid deviceId, HomeCommand command, CancellationToken ct = default)
{
Commands.Add((deviceId, command));
if (CommandHandler is { } handler)
{
return Task.FromResult(handler(deviceId, command));
}
var state = command switch
{
HomeCommand.TurnOn => On(deviceId),
HomeCommand.TurnOff => Off(deviceId),
HomeCommand.Toggle => States.TryGetValue(deviceId, out var s) && s.IsOn is true ? Off(deviceId) : On(deviceId),
HomeCommand.Open => Cover(deviceId, "opening", 50),
HomeCommand.Close => Cover(deviceId, "closing", 50),
_ => Cover(deviceId, "stopped", 50),
};
States[deviceId] = state;
return Task.FromResult(ApiResult<HomeCommandResultDto>.Ok(new HomeCommandResultDto(true, null, state)));
}
public Task<ApiResult<List<HomeAdminDeviceDto>>> GetDevicesAsync(CancellationToken ct = default) => Task.FromResult(ApiResult<List<HomeAdminDeviceDto>>.Ok([.. Devices]));
public Task<ApiResult<HomeAdminDeviceDto>> CreateDeviceAsync(SaveHomeDeviceRequest request, CancellationToken ct = default)
{
if (SaveProblem is { } problem)
{
return Task.FromResult(ApiResult<HomeAdminDeviceDto>.Fail(problem));
}
SavedDevices.Add(request);
var (host, _) = HomeRules.NormalizeHost(request.Host);
var dto = new HomeAdminDeviceDto(Guid.NewGuid(), request.Name, request.Kind, request.Icon, request.RoomId, request.Driver, request.Generation, host!, request.Channel,
request.AuthUser, request.Password is not null, request.SortOrder, [1], []);
Devices.Add(dto);
return Task.FromResult(ApiResult<HomeAdminDeviceDto>.Ok(dto));
}
public Task<ApiResult<HomeAdminDeviceDto>> UpdateDeviceAsync(Guid id, SaveHomeDeviceRequest request, CancellationToken ct = default)
{
if (SaveProblem is { } problem)
{
return Task.FromResult(ApiResult<HomeAdminDeviceDto>.Fail(problem));
}
SavedDevices.Add(request);
var index = Devices.FindIndex(d => d.Id == id);
Devices[index] = Devices[index] with
{
Name = request.Name, Kind = request.Kind, Icon = request.Icon, RoomId = request.RoomId, Generation = request.Generation, Host = HomeRules.NormalizeHost(request.Host).Host!,
Channel = request.Channel, AuthUser = request.AuthUser, HasPassword = request.ClearPassword ? false : request.Password is not null || Devices[index].HasPassword,
};
return Task.FromResult(ApiResult<HomeAdminDeviceDto>.Ok(Devices[index]));
}
public Task<ApiResult<bool>> DeleteDeviceAsync(Guid id, CancellationToken ct = default)
{
Deleted.Add(id);
Devices.RemoveAll(d => d.Id == id);
return Task.FromResult(ApiResult<bool>.Ok(true));
}
public List<Guid> Tested { get; } = [];
public Task<ApiResult<HomeTestResultDto>> TestDeviceAsync(Guid id, CancellationToken ct = default)
{
Tested.Add(id);
return Task.FromResult(ApiResult<HomeTestResultDto>.Ok(TestResult));
}
public Task<ApiResult<HomeTestResultDto>> TestDraftAsync(TestHomeDeviceRequest request, CancellationToken ct = default)
{
DraftTests.Add(request);
return Task.FromResult(ApiResult<HomeTestResultDto>.Ok(TestResult));
}
public Task<ApiResult<HomeAdminDeviceDto>> SetAccessAsync(Guid id, SetHomeAccessRequest request, CancellationToken ct = default)
{
SavedAccess.Add((id, request));
var index = Devices.FindIndex(d => d.Id == id);
Devices[index] = Devices[index] with { Grants = request.Grants };
return Task.FromResult(ApiResult<HomeAdminDeviceDto>.Ok(Devices[index]));
}
public Task<ApiResult<List<UserDirectoryEntry>>> GetUsersAsync(CancellationToken ct = default) => Task.FromResult(ApiResult<List<UserDirectoryEntry>>.Ok([.. Users]));
public Task<ApiResult<List<HomeAdminRoomDto>>> GetRoomsAsync(CancellationToken ct = default) => Task.FromResult(ApiResult<List<HomeAdminRoomDto>>.Ok([.. Rooms]));
public Task<ApiResult<HomeAdminRoomDto>> CreateRoomAsync(SaveHomeRoomRequest request, CancellationToken ct = default)
{
SavedRooms.Add(request);
var room = new HomeAdminRoomDto(Guid.NewGuid(), request.Name, request.SortOrder, 0);
Rooms.Add(room);
return Task.FromResult(ApiResult<HomeAdminRoomDto>.Ok(room));
}
public Task<ApiResult<bool>> DeleteRoomAsync(Guid id, CancellationToken ct = default)
{
Deleted.Add(id);
Rooms.RemoveAll(r => r.Id == id);
return Task.FromResult(ApiResult<bool>.Ok(true));
}
public Task<ApiResult<List<HomeAdminPlanDto>>> GetPlansAsync(CancellationToken ct = default) => Task.FromResult(ApiResult<List<HomeAdminPlanDto>>.Ok([.. Plans]));
public Task<ApiResult<HomeAdminPlanDto>> SetPlacementsAsync(Guid id, SaveHomePlacementsRequest request, CancellationToken ct = default)
{
SavedPlacements.Add((id, request));
var index = Plans.FindIndex(p => p.Id == id);
Plans[index] = Plans[index] with { Pins = request.Pins };
return Task.FromResult(ApiResult<HomeAdminPlanDto>.Ok(Plans[index]));
}
public Task<ApiResult<bool>> DeletePlanAsync(Guid id, CancellationToken ct = default)
{
Deleted.Add(id);
Plans.RemoveAll(p => p.Id == id);
return Task.FromResult(ApiResult<bool>.Ok(true));
}
public List<HomeActionLogDto> Log { get; } = [];
public Task<ApiResult<PagedResult<HomeActionLogDto>>> GetLogAsync(int page, int pageSize, CancellationToken ct = default) =>
Task.FromResult(ApiResult<PagedResult<HomeActionLogDto>>.Ok(new PagedResult<HomeActionLogDto>([.. Log], Log.Count, page, pageSize)));
public static HomeAdminDeviceDto AdminDevice(string name, HomeDeviceKind kind = HomeDeviceKind.Switch, Guid? room = null, IReadOnlyList<HomeGrantDto>? grants = null, bool hasPassword = false) =>
new(Guid.NewGuid(), name, kind, null, room, HomeDriverType.Shelly, 2, "http://192.168.1.50", 0, hasPassword ? "admin" : null, hasPassword, 0, [1], grants ?? []);
public static HomeAdminDeviceDto ValloxDevice(string name) =>
new(Guid.NewGuid(), name, HomeDeviceKind.Ventilation, null, null, HomeDriverType.Vallox, 1, "http://192.168.1.60", 0, null, false, 0, [1], []);
public static UserDirectoryEntry User(string name, string role = Roles.Child) => new(Guid.NewGuid(), name, "#43A047", null, role);
}