Skip to content

Commit

Permalink
feat: UI for update interval configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
sergey-tihon committed Jan 2, 2023
1 parent d14e01d commit 312ece4
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 56 deletions.
9 changes: 6 additions & 3 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions src/StreamDeck.Homebridge/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type GlobalSettings = {
Host: string
UserName: string
Password: string
UpdateInterval: int
}

type ActionSetting = {
Expand Down
35 changes: 32 additions & 3 deletions src/StreamDeck.Homebridge/PiView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
126 changes: 76 additions & 50 deletions src/StreamDeck.Homebridge/PluginAgent.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ module StreamDeck.Homebridge.PluginAgent
open Browser.Dom
open StreamDeck.SDK
open StreamDeck.SDK.PluginModel
open System

type PluginInnerState = {
replyAgent: MailboxProcessor<PluginOutEvent>
client: Client.HomebridgeClient option
lastCharacteristicUpdate: DateTime
characteristics: Map<string * string, Client.AccessoryServiceCharacteristic>
visibleActions: Map<string, Domain.ActionSetting * int option>
updateInterval: int
timerId: float option
}

Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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

Expand All @@ -117,14 +129,32 @@ let updateState(state: PluginInnerState) = async {

return
{ state with
characteristics = characteristics
visibleActions = visibleActions
}
}


let createPluginAgent() : MailboxProcessor<PluginInEvent> =
let mutable agent: MailboxProcessor<PluginInEvent> 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 {
Expand All @@ -137,8 +167,10 @@ let createPluginAgent() : MailboxProcessor<PluginInEvent> =
let state = {
replyAgent = replyAgent
client = None
lastCharacteristicUpdate = DateTime.MinValue
characteristics = Map.empty
visibleActions = Map.empty
updateInterval = 0
timerId = None
}

Expand All @@ -155,23 +187,27 @@ let createPluginAgent() : MailboxProcessor<PluginInEvent> =
match msg with
| PluginInEvent.DidReceiveGlobalSettings settings ->
let state =
{ state with
client =
Domain.tryParse<Domain.GlobalSettings>(settings)
|> Option.map(Client.HomebridgeClient)
}
match Domain.tryParse<Domain.GlobalSettings>(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 =
Expand All @@ -180,18 +216,8 @@ let createPluginAgent() : MailboxProcessor<PluginInEvent> =
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, _) ->
Expand Down

0 comments on commit 312ece4

Please sign in to comment.