- 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
213 lines
9.9 KiB
C#
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);
|
|
}
|