diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 704297b..78c4a83 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,10 @@ -## 1.4.0 - 26 Dec 2022 +## 1.4.0 - 2 Jan 2023 +- Added configurable interval for Switch action state auto-update #7 +- Removed analytics #20 +- Removed warning response when Homebridge does not change state immediately +- Auth token caching - F# SDK aligned with [Changes in Stream Deck 6.0](https://developer.elgato.com/documentation/stream-deck/sdk/changelog/) -- Removed analytics -- Dependencies update +- Dependencies update (Fable 4, React 18, Feliz 2, Elmish 4 and more) ## 1.3.1 - 27 Feb 2022 - Fix version in manifest.json diff --git a/src/StreamDeck.Homebridge/Domain.fs b/src/StreamDeck.Homebridge/Domain.fs index d126e10..9d0c919 100644 --- a/src/StreamDeck.Homebridge/Domain.fs +++ b/src/StreamDeck.Homebridge/Domain.fs @@ -6,6 +6,7 @@ type GlobalSettings = { Host: string UserName: string Password: string + UpdateInterval: int } type ActionSetting = { diff --git a/src/StreamDeck.Homebridge/PiView.fs b/src/StreamDeck.Homebridge/PiView.fs index ebfc259..8199e01 100644 --- a/src/StreamDeck.Homebridge/PiView.fs +++ b/src/StreamDeck.Homebridge/PiView.fs @@ -54,6 +54,7 @@ let init isDevMode = Host = "http://192.168.0.55:8581" UserName = "admin" Password = "admin" + UpdateInterval = 5 } Client = Error null IsLoading = Ok false @@ -508,15 +509,43 @@ let render (model: PiModel) (dispatch: PiMsg -> unit) = prop.value model.ServerInfo.Password prop.required true prop.onChange(fun value -> - dispatch - <| PiMsg.UpdateServerInfo + let settings = { model.ServerInfo with Password = value - }) + } + + dispatch <| PiMsg.UpdateServerInfo settings) + ] + ] + ] + + Html.div [ + prop.className SdPi.Item + prop.children [ + Html.div [ prop.className SdPi.ItemLabel; prop.text "Update" ] + Html.select [ + prop.classes [ SdPi.ItemValue; "select" ] + prop.value model.ServerInfo.UpdateInterval + prop.onChange(fun (value: string) -> + let settings = + { model.ServerInfo with + UpdateInterval = int(value) + } + + dispatch <| PiMsg.UpdateServerInfo settings) + prop.children [ + Html.option [ prop.value "0"; prop.text "Never" ] + Html.option [ prop.value "1"; prop.text "Every second" ] + Html.option [ prop.value "2"; prop.text "Every 2 seconds" ] + Html.option [ prop.value "5"; prop.text "Every 5 seconds" ] + Html.option [ prop.value "10"; prop.text "Every 10 seconds" ] + Html.option [ prop.value "60"; prop.text "Every minute" ] + ] ] ] ] + Html.div [ prop.className SdPi.Item prop.type' "button" diff --git a/src/StreamDeck.Homebridge/PluginAgent.fs b/src/StreamDeck.Homebridge/PluginAgent.fs index ba2549d..ae6f5bb 100644 --- a/src/StreamDeck.Homebridge/PluginAgent.fs +++ b/src/StreamDeck.Homebridge/PluginAgent.fs @@ -3,12 +3,15 @@ module StreamDeck.Homebridge.PluginAgent open Browser.Dom open StreamDeck.SDK open StreamDeck.SDK.PluginModel +open System type PluginInnerState = { replyAgent: MailboxProcessor client: Client.HomebridgeClient option + lastCharacteristicUpdate: DateTime characteristics: Map visibleActions: Map + updateInterval: int timerId: float option } @@ -39,15 +42,12 @@ let processKeyUp (state: PluginInnerState) (event: Dto.Event) (payload: Dto.Acti let targetValue = 1 - currentValue match! client.SetAccessoryCharacteristic accessoryId characteristicType targetValue with - | Ok accessory' -> - let ch' = accessory' |> PiView.getCharacteristic characteristicType - let currentValue' = ch'.value.Value :?> int - - if currentValue = currentValue' then - state.replyAgent.Post <| PluginOutEvent.ShowAlert event.context - else - state.replyAgent.Post - <| PluginOutEvent.SetState(event.context, currentValue') + | Ok accessory -> + let ch' = accessory |> PiView.getCharacteristic characteristicType + let updatedValue = ch'.value.Value :?> int + + if targetValue = updatedValue then + state.replyAgent.Post <| PluginOutEvent.ShowOk event.context | Error e -> onError e | _ -> onError $"Cannot find characteristic by id '{accessoryId}, {characteristicType}'." | _ -> onError "Action is not properly configured" @@ -66,33 +66,45 @@ let processKeyUp (state: PluginInnerState) (event: Dto.Event) (payload: Dto.Acti let ch = accessory |> PiView.getCharacteristic characteristicType let currentValue = ch.value.Value :?> float - if abs(targetValue - currentValue) > 1e-8 then - state.replyAgent.Post <| PluginOutEvent.ShowAlert event.context - else + if abs(targetValue - currentValue) < 1e-8 then state.replyAgent.Post <| PluginOutEvent.ShowOk event.context | Error e -> onError e | _ -> onError "Action is not properly configured" | _ -> onError $"Action {event.action} is not yet supported" } -let updateState(state: PluginInnerState) = async { - let! accessories = - state.client - |> Option.map(fun client -> client.GetAccessories()) - |> Option.defaultValue(async { return Error("Homedbridge client is not set yet") }) - - let characteristics = - match accessories with - | Error _ -> state.characteristics - | Ok(accessories) -> - accessories - |> Array.collect(fun accessory -> - accessory.serviceCharacteristics - |> Array.map(fun characteristic -> - let key = accessory.uniqueId, characteristic.``type`` - key, characteristic)) - |> Map.ofArray +let updateAccessories(state: PluginInnerState) = async { + let now = DateTime.Now + + if now - state.lastCharacteristicUpdate < TimeSpan.FromSeconds 1.0 then + return state + else + let! accessories = + state.client + |> Option.map(fun client -> client.GetAccessories()) + |> Option.defaultValue(async { return Error("Homedbridge client is not set yet") }) + + let characteristics = + match accessories with + | Error _ -> state.characteristics + | Ok(accessories) -> + accessories + |> Array.collect(fun accessory -> + accessory.serviceCharacteristics + |> Array.map(fun characteristic -> + let key = accessory.uniqueId, characteristic.``type`` + key, characteristic)) + |> Map.ofArray + + return + { state with + characteristics = characteristics + lastCharacteristicUpdate = now + } +} +let updateActions(state: PluginInnerState) = async { + let! state = updateAccessories state let visibleActions = state.visibleActions @@ -103,7 +115,7 @@ let updateState(state: PluginInnerState) = async { CharacteristicType = Some characteristicType }, Some actionState -> - match characteristics |> Map.tryFind(accessoryId, characteristicType) with + match state.characteristics |> Map.tryFind(accessoryId, characteristicType) with | Some(ch) when ch.value.IsSome -> let chValue = ch.value.Value :?> int @@ -117,14 +129,32 @@ let updateState(state: PluginInnerState) = async { return { state with - characteristics = characteristics visibleActions = visibleActions } } + let createPluginAgent() : MailboxProcessor = let mutable agent: MailboxProcessor option = None + let updateTimer(state: PluginInnerState) = + if state.timerId.IsSome then + window.clearTimeout(state.timerId.Value) + + let timerId = + if state.updateInterval = 0 then + None + else + window.setInterval( + (fun _ -> agent.Value.Post(PluginInEvent.SystemDidWakeUp)), + 1000 * state.updateInterval, + [||] + ) + |> Some + + { state with timerId = timerId } + + agent <- MailboxProcessor.Start(fun inbox -> let rec idle() = async { @@ -137,8 +167,10 @@ let createPluginAgent() : MailboxProcessor = let state = { replyAgent = replyAgent client = None + lastCharacteristicUpdate = DateTime.MinValue characteristics = Map.empty visibleActions = Map.empty + updateInterval = 0 timerId = None } @@ -155,23 +187,27 @@ let createPluginAgent() : MailboxProcessor = match msg with | PluginInEvent.DidReceiveGlobalSettings settings -> let state = - { state with - client = - Domain.tryParse(settings) - |> Option.map(Client.HomebridgeClient) - } + match Domain.tryParse(settings) with + | Some(settings) -> + { state with + client = Some(Client.HomebridgeClient settings) + updateInterval = settings.UpdateInterval + } + |> updateTimer + | _ -> state return! loop state | PluginInEvent.KeyUp(event, payload) -> - let! state = updateState state + let! state = updateActions state do! processKeyUp state event payload - // TODO: update state of changed action return! loop state | PluginInEvent.SystemDidWakeUp -> // Fake action triggered by timer to update buttons state - let! state = updateState state + let! state = updateActions state return! loop state | PluginInEvent.WillAppear(event, payload) -> + let! state = updateActions state + let state = { state with visibleActions = @@ -180,18 +216,8 @@ let createPluginAgent() : MailboxProcessor = state.visibleActions |> Map.add event.context (actionSetting, payload.state) | _ -> state.visibleActions - timerId = - match state.timerId with - | Some _ -> state.timerId - | None -> - Some( - window.setInterval( - (fun _ -> agent.Value.Post(PluginInEvent.SystemDidWakeUp)), - 5_000, - [||] - ) - ) } + |> updateTimer return! loop state | PluginInEvent.WillDisappear(event, _) ->