diff --git a/NightLight.Core.Tests/FakeHome.fs b/NightLight.Core.Tests/FakeHome.fs new file mode 100644 index 0000000..df2da1b --- /dev/null +++ b/NightLight.Core.Tests/FakeHome.fs @@ -0,0 +1,81 @@ +namespace NightLight.Core.Tests + +open System +open System.Text.RegularExpressions +open NightLight.Core.Models +open FsToolkit.ErrorHandling +open FSharp.Data + +type HumanInteraction = + | LightTurnedOn of Light + | LightTurnedOff of Light + +type Interaction = + | HumanInteraction of HumanInteraction + | TimeChanged of DateTime + +type LightState = + | Off + | On of Brightness: byte + +type FakeLight(light: Light) = + let mutable hasPower = false + let mutable brightness: byte = 255uy + + member _.LightWithState = light, if hasPower then On brightness else Off + + member _.TurnOn() = hasPower <- true + + member _.TurnOff() = hasPower <- false + + member _.SetBrightness(newBrightness: byte) = + if hasPower then + brightness <- newBrightness + +type FakeHome(now: DateTime) = + let nightLightStateMachine = NightLightStateMachine now + + let friendlyNameToFakeLight = + lights + |> Seq.map (fun light -> light.FriendlyName, FakeLight light) + |> Map.ofSeq + + let processCommand command = + option { + let! friendlyName = + let m = Regex.Match(command.Topic, "^zigbee2mqtt/(.+)/set$") + if m.Success then Some m.Groups.[1].Value else None + + let! fakeLight = Map.tryFind friendlyName friendlyNameToFakeLight + + let parsedPayload = JsonValue.Parse command.Payload + + match parsedPayload.TryGetProperty "brightness" with + | Some(JsonValue.Number newBrightness) -> fakeLight.SetBrightness(byte newBrightness) + | None -> () + | value -> failwith $"Unexpected brightness value {value}" + } + |> ignore + + member _.LightStates = friendlyNameToFakeLight.Values |> Seq.map _.LightWithState + + member _.Interact(interaction: Interaction) : Result = + result { + match interaction with + | HumanInteraction(LightTurnedOn light) -> + friendlyNameToFakeLight[light.FriendlyName].TurnOn() + + do! + { Topic = "zigbee2mqtt/bridge/event" + Payload = + $@"{{ + ""type"": ""device_announce"", + ""data"": {{ ""friendly_name"": ""{light.FriendlyName}"" }} + }}" } + |> nightLightStateMachine.SendMessage + | HumanInteraction(LightTurnedOff light) -> friendlyNameToFakeLight[light.FriendlyName].TurnOff() + | TimeChanged time -> do! nightLightStateMachine.ChangeTime time + + nightLightStateMachine.TransmittedCommands |> Seq.iter processCommand + nightLightStateMachine.ClearTransmittedCommands() + } diff --git a/NightLight.Core.Tests/NightLight.Core.Tests.fsproj b/NightLight.Core.Tests/NightLight.Core.Tests.fsproj new file mode 100644 index 0000000..4979a7a --- /dev/null +++ b/NightLight.Core.Tests/NightLight.Core.Tests.fsproj @@ -0,0 +1,27 @@ + + + + Exe + net9.0 + true + + + + + + + + + + + + + + + + + + + + + diff --git a/NightLight.Core.Tests/NightLightStateMachine.fs b/NightLight.Core.Tests/NightLightStateMachine.fs new file mode 100644 index 0000000..0379a3b --- /dev/null +++ b/NightLight.Core.Tests/NightLightStateMachine.fs @@ -0,0 +1,28 @@ +namespace NightLight.Core.Tests + +open System +open System.Collections.Generic +open FsToolkit.ErrorHandling +open NightLight.Core.Models +open NightLight.Core.Core + +type NightLightStateMachine(now: DateTime) = + let mutable state = { Time = now } + + let transmittedCommands = new List() + + let sendEvent event = + result { + let! newState, commands = onEventReceived state event + state <- newState + transmittedCommands.AddRange commands + } + + member _.TransmittedCommands = transmittedCommands.AsReadOnly() + + member _.SendMessage message = + ReceivedZigbeeEvent message |> sendEvent + + member _.ChangeTime time = TimeChanged time |> sendEvent + + member _.ClearTransmittedCommands() = transmittedCommands.Clear() diff --git a/NightLight.sln b/NightLight.sln index 0918513..7985b44 100644 --- a/NightLight.sln +++ b/NightLight.sln @@ -4,6 +4,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NightLight", "NightLight\Ni EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NightLight.Core", "NightLight.Core\NightLight.Core.fsproj", "{FE406BDE-B6C1-4BDA-A29D-54D50A7828A9}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NightLight.Core.Tests", "NightLight.Core.Tests\NightLight.Core.Tests.fsproj", "{23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,18 @@ Global {FE406BDE-B6C1-4BDA-A29D-54D50A7828A9}.Release|x64.Build.0 = Release|Any CPU {FE406BDE-B6C1-4BDA-A29D-54D50A7828A9}.Release|x86.ActiveCfg = Release|Any CPU {FE406BDE-B6C1-4BDA-A29D-54D50A7828A9}.Release|x86.Build.0 = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|x64.ActiveCfg = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|x64.Build.0 = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Debug|x86.Build.0 = Debug|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|Any CPU.Build.0 = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|x64.ActiveCfg = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|x64.Build.0 = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|x86.ActiveCfg = Release|Any CPU + {23C7B106-B1B8-49A8-B2CF-22C078C8DDB3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE