module NightLight.Core.Core open System open NightLight.Core.Models open NightLight.Core.PartsOfDay open NightLight.Core.ZigbeeEvents open NightLight.Core.ZigbeeCommands open NightLight.Core.Moods open FsToolkit.ErrorHandling let internal tryFindLight friendlyName = Seq.tryFind (fun light -> (lightProps light).FriendlyName = friendlyName) lights let internal generateZigbeeCommandsToFixLight (light: Light) (desiredLightSettings: LightSettings) = seq { if desiredLightSettings.State = Off then yield generateStateCommand desiredLightSettings.State light if desiredLightSettings.State = On then yield generateBrightnessCommand light desiredLightSettings.Brightness yield generateColorCommand light desiredLightSettings.Color } type internal NightLightState = { Time: DateTime Alarm: bool LightToManualState: Map } let internal computeLightSettings (light: Light) (nightLightState: NightLightState) = let partOfDay = getPartOfDay nightLightState.Time let color, brightness = getDesiredMood (lightProps light).Room partOfDay |> getDesiredColorAndBrightness (lightProps light).Bulb { Color = color Brightness = if nightLightState.Alarm && (light = RightBedroomLamp || light = LeftBedroomLamp) then brightness.Scale(getAlarmWeight nightLightState.Time) else brightness State = if nightLightState.Alarm && (light = RightBedroomLamp || light = LeftBedroomLamp) then On else nightLightState.LightToManualState[light] } let internal withStateFor (light: Light) (state: State) (oldNightLightState: NightLightState) = { oldNightLightState with LightToManualState = Map.add light state oldNightLightState.LightToManualState } let internal withAlarmOff (oldNightLightState: NightLightState) = { oldNightLightState with Alarm = false } let internal generateZigbeeCommandsForDifference (maybeBefore: NightLightState option) (after: NightLightState) = lights |> Seq.collect (fun light -> let oldLightSettings = maybeBefore |> Option.map (computeLightSettings light) let newLightSettings = after |> computeLightSettings light if oldLightSettings <> Some newLightSettings then generateZigbeeCommandsToFixLight light newLightSettings else Seq.empty) type NightLightStateMachine private (maybeState: NightLightState option) = new() = NightLightStateMachine None member this.OnEventReceived(event: Event) : Result = result { match event, maybeState with | ReceivedZigbeeEvent payload, Some currentState -> let! zigbeeEvent = parseZigbeeEvent payload |> Result.mapError ParseZigbeeEventError return match zigbeeEvent with | DeviceAnnounce friendlyName -> let maybeLight = tryFindLight friendlyName this, match maybeLight with | Some light -> currentState |> computeLightSettings light |> generateZigbeeCommandsToFixLight light | None -> Seq.empty | ButtonPress action -> let newNightLightState = match action with | PressedOn -> currentState |> withAlarmOff |> withStateFor RightBedroomLamp On |> withStateFor LeftBedroomLamp On | PressedOff -> currentState |> withAlarmOff |> withStateFor RightBedroomLamp Off |> withStateFor LeftBedroomLamp Off | PressedLeft -> currentState |> withAlarmOff |> withStateFor RightBedroomLamp Off |> withStateFor LeftBedroomLamp On | PressedRight -> currentState |> withStateFor LivingRoomWallLamp Off |> withStateFor LivingRoomFloorLamp Off NightLightStateMachine(Some newNightLightState), generateZigbeeCommandsForDifference (Some currentState) newNightLightState | TimeChanged newTime, maybeCurrentState -> let alarm = let newDayStarted = let newPartOfDay = getPartOfDay newTime let maybePreviousPartOfDay = maybeCurrentState |> Option.map _.Time |> Option.map getPartOfDay maybePreviousPartOfDay = Some Night && newPartOfDay = Day newDayStarted || maybeCurrentState |> Option.map _.Alarm |> Option.defaultValue false let newNightLightState = { Time = newTime Alarm = alarm LightToManualState = maybeCurrentState |> Option.map _.LightToManualState |> Option.defaultValue (lights |> Seq.map (fun light -> light, On) |> Map.ofSeq) } return NightLightStateMachine(Some newNightLightState), generateZigbeeCommandsForDifference maybeCurrentState newNightLightState | _, None -> return! Error TimeIsUnknown }