Add naive remote control implementation

This commit is contained in:
Sven van Heugten 2026-01-05 21:53:48 +01:00
parent d2864fcc22
commit 838bbb79a3
5 changed files with 78 additions and 26 deletions

View file

@ -9,6 +9,7 @@ type Event =
| TimeChanged of DateTime | TimeChanged of DateTime
type ParseZigbeeEventError = type ParseZigbeeEventError =
| UnknownTopic
| InvalidJson | InvalidJson
| MissingTypeField | MissingTypeField
| MissingDataField | MissingDataField
@ -16,6 +17,8 @@ type ParseZigbeeEventError =
| InvalidTypeField | InvalidTypeField
| InvalidFriendlyNameField | InvalidFriendlyNameField
| UnknownType | UnknownType
| MissingActionField
| InvalidActionField
type OnEventReceivedError = type OnEventReceivedError =
| ParseZigbeeEventError of ParseZigbeeEventError | ParseZigbeeEventError of ParseZigbeeEventError
@ -40,21 +43,29 @@ type DeviceFriendlyName =
type Light = type Light =
{ FriendlyName: DeviceFriendlyName { FriendlyName: DeviceFriendlyName
Room: Room Room: Room
Bulb: Bulb } Bulb: Bulb
ControlledWithRemote: bool }
let lights = let lights =
[ { FriendlyName = DeviceFriendlyName "Vardagsrum - Fönsterlampa" [ { FriendlyName = DeviceFriendlyName "Vardagsrum - Fönsterlampa"
Room = LivingRoom Room = LivingRoom
Bulb = IkeaBulb } Bulb = IkeaBulb
ControlledWithRemote = true }
{ FriendlyName = DeviceFriendlyName "Vardagsrum - Vägglampa" { FriendlyName = DeviceFriendlyName "Vardagsrum - Vägglampa"
Room = LivingRoom Room = LivingRoom
Bulb = PaulmannBulb } Bulb = PaulmannBulb
ControlledWithRemote = false }
{ FriendlyName = DeviceFriendlyName "Vardagsrum - Golvlampa" { FriendlyName = DeviceFriendlyName "Vardagsrum - Golvlampa"
Room = LivingRoom Room = LivingRoom
Bulb = PaulmannBulb } Bulb = PaulmannBulb
ControlledWithRemote = false }
{ FriendlyName = DeviceFriendlyName "Badrum - Taklampa" { FriendlyName = DeviceFriendlyName "Badrum - Taklampa"
Room = Bathroom Room = Bathroom
Bulb = IkeaBulb } Bulb = IkeaBulb
ControlledWithRemote = false }
{ FriendlyName = DeviceFriendlyName "Sovrum - Nattduksbordlampa" { FriendlyName = DeviceFriendlyName "Sovrum - Nattduksbordlampa"
Room = Bedroom Room = Bedroom
Bulb = IkeaBulb } ] Bulb = IkeaBulb
ControlledWithRemote = true } ]
let remoteControlFriendlyName = DeviceFriendlyName "Fjärrkontroll"

View file

@ -15,7 +15,7 @@ let internal generateZigbeeCommandToFixLight partOfDay light =
let color, brightness = let color, brightness =
getDesiredMood light.Room partOfDay |> getDesiredColorAndBrightness light.Bulb getDesiredMood light.Room partOfDay |> getDesiredColorAndBrightness light.Bulb
generateZigbeeCommand light.FriendlyName color brightness generateZigbeeCommand color brightness light
type NightLightStateMachine private (maybeTime: DateTime option) = type NightLightStateMachine private (maybeTime: DateTime option) =
new() = NightLightStateMachine None new() = NightLightStateMachine None
@ -37,6 +37,12 @@ type NightLightStateMachine private (maybeTime: DateTime option) =
match maybeLight with match maybeLight with
| Some light -> generateZigbeeCommandToFixLight partOfDay light |> Seq.singleton | Some light -> generateZigbeeCommandToFixLight partOfDay light |> Seq.singleton
| None -> Seq.empty | None -> Seq.empty
| ButtonPress action ->
let remoteControlledLights = lights |> Seq.filter _.ControlledWithRemote
match action with
| PressedOn -> remoteControlledLights |> Seq.map (generateStateCommand On)
| PressedOff -> remoteControlledLights |> Seq.map (generateStateCommand Off)
| TimeChanged newTime, maybePartOfDay -> | TimeChanged newTime, maybePartOfDay ->
let newState = NightLightStateMachine(Some newTime) let newState = NightLightStateMachine(Some newTime)
let newPartOfDay = getPartOfDay newTime let newPartOfDay = getPartOfDay newTime

View file

@ -4,7 +4,28 @@ open System.Text.Json.Nodes
open NightLight.Core.Models open NightLight.Core.Models
open NightLight.Core.Moods open NightLight.Core.Moods
let generateZigbeeCommand (friendlyName: DeviceFriendlyName) targetColor targetBrightness = type State =
| On
| Off
let toZigbeeCommand light payload =
let topic = $"zigbee2mqtt/{light.FriendlyName.Get}/set"
{ Topic = topic; Payload = payload }
let generateStateCommand state light =
let commandObj = JsonObject()
commandObj["state"] <-
match state with
| On -> "ON"
| Off -> "OFF"
if light.Bulb = IkeaBulb then
commandObj["transition"] <- 0
commandObj.ToJsonString() |> toZigbeeCommand light
let generateZigbeeCommand targetColor targetBrightness light =
let commandObj = JsonObject() let commandObj = JsonObject()
match targetColor with match targetColor with
@ -19,7 +40,4 @@ let generateZigbeeCommand (friendlyName: DeviceFriendlyName) targetColor targetB
match targetBrightness with match targetBrightness with
| Brightness b -> b | Brightness b -> b
let topic = $"zigbee2mqtt/{friendlyName.Get}/set" commandObj.ToJsonString() |> toZigbeeCommand light
let payload = commandObj.ToJsonString()
{ Topic = topic; Payload = payload }

View file

@ -4,22 +4,38 @@ open NightLight.Core.Models
open FsToolkit.ErrorHandling open FsToolkit.ErrorHandling
open FSharp.Data open FSharp.Data
type ZigbeeEvent = DeviceAnnounce of DeviceFriendlyName type Action =
| PressedOn
| PressedOff
type ZigbeeEvent =
| DeviceAnnounce of DeviceFriendlyName
| ButtonPress of Action
let parseZigbeeEvent (message: Message) = let parseZigbeeEvent (message: Message) =
result { result {
let! jsonValue = JsonValue.TryParse message.Payload |> Result.requireSome InvalidJson let! jsonValue = JsonValue.TryParse message.Payload |> Result.requireSome InvalidJson
let! messageType = jsonValue.TryGetProperty "type" |> Result.requireSome MissingTypeField match message.Topic with
let! messageData = jsonValue.TryGetProperty "data" |> Result.requireSome MissingDataField | "zigbee2mqtt/bridge/event" ->
let! messageType = jsonValue.TryGetProperty "type" |> Result.requireSome MissingTypeField
let! messageData = jsonValue.TryGetProperty "data" |> Result.requireSome MissingDataField
return! return!
match messageType with match messageType with
| JsonValue.String "device_announce" -> | JsonValue.String "device_announce" ->
match messageData.TryGetProperty "friendly_name" with match messageData.TryGetProperty "friendly_name" with
| Some(JsonValue.String friendlyName) -> Ok <| DeviceAnnounce(DeviceFriendlyName friendlyName) | Some(JsonValue.String friendlyName) -> Ok <| DeviceAnnounce(DeviceFriendlyName friendlyName)
| Some _ -> Error InvalidFriendlyNameField | Some _ -> Error InvalidFriendlyNameField
| None -> Error MissingFriendlyNameField | None -> Error MissingFriendlyNameField
| JsonValue.String _ -> Error UnknownType | JsonValue.String _ -> Error UnknownType
| _ -> Error InvalidTypeField | _ -> Error InvalidTypeField
| "zigbee2mqtt/Fjärrkontroll" ->
return!
match jsonValue.TryGetProperty "action" with
| Some(JsonValue.String "on") -> Ok(ButtonPress PressedOn)
| Some(JsonValue.String "off") -> Ok(ButtonPress PressedOff)
| Some _ -> Error InvalidActionField
| None -> Error MissingActionField
| _ -> return! Error UnknownTopic
} }

View file

@ -104,8 +104,9 @@ let mainAsync _ =
do! mqttClient.ConnectAsync mqttClientOptions |> Async.AwaitTask |> Async.Ignore do! mqttClient.ConnectAsync mqttClientOptions |> Async.AwaitTask |> Async.Ignore
do! do!
mqttClient.SubscribeAsync "zigbee2mqtt/bridge/event" [ "zigbee2mqtt/bridge/event"; $"zigbee2mqtt/{remoteControlFriendlyName.Get}" ]
|> Async.AwaitTask |> Seq.map (fun topic -> async { return! mqttClient.SubscribeAsync topic |> Async.AwaitTask })
|> Async.Sequential
|> Async.Ignore |> Async.Ignore
while true do while true do