Implement the Right button on the bedroom remote

This commit is contained in:
Sven van Heugten 2026-02-27 19:09:13 +01:00
parent b9fc8bfaea
commit d3a00b8a77
5 changed files with 74 additions and 64 deletions

View file

@ -10,6 +10,7 @@ type RemoteInteraction =
| RemotePressedOnButton | RemotePressedOnButton
| RemotePressedOffButton | RemotePressedOffButton
| RemotePressedLeftButton | RemotePressedLeftButton
| RemotePressedRightButton
type HumanInteraction = type HumanInteraction =
| LightPoweredOn of Light | LightPoweredOn of Light
@ -49,8 +50,6 @@ type FakeLight(light: Light) =
member _.SetBrightness(newBrightness: byte) = member _.SetBrightness(newBrightness: byte) =
if hasPower then if hasPower then
brightness <- newBrightness brightness <- newBrightness
if (lightProps light).Bulb = IkeaBulb then
state <- true state <- true
member _.SetColor(newColor: Color) = member _.SetColor(newColor: Color) =
@ -139,6 +138,11 @@ type FakeHome() =
Payload = @"{ ""action"": ""arrow_left_click"" }" } Payload = @"{ ""action"": ""arrow_left_click"" }" }
|> ReceivedZigbeeEvent |> ReceivedZigbeeEvent
|> onEventPublished.Trigger |> onEventPublished.Trigger
| RemoteInteraction RemotePressedRightButton ->
{ Topic = $"zigbee2mqtt/{remoteControlFriendlyName.Get}"
Payload = @"{ ""action"": ""arrow_right_click"" }" }
|> ReceivedZigbeeEvent
|> onEventPublished.Trigger
| TimeChanged newTime -> newTime |> Event.TimeChanged |> onEventPublished.Trigger | TimeChanged newTime -> newTime |> Event.TimeChanged |> onEventPublished.Trigger
type FakeHome with type FakeHome with
@ -147,19 +151,6 @@ type FakeHome with
member this.LightsThatAreOn = member this.LightsThatAreOn =
this.LightStates |> Seq.filter (snd >> _.IsOn) |> Seq.toList this.LightStates |> Seq.filter (snd >> _.IsOn) |> Seq.toList
member this.NonRemotelyControlledLightStates =
this.LightStates
|> Seq.filter (fun (light, _) ->
light = LivingRoomWallLamp
|| light = LivingRoomFloorLamp
|| light = BathroomCeilingLamp)
|> Seq.toList
member this.RemotelyControlledLightStates =
this.LightStates
|> Seq.filter (fun (light, _) -> light = RightBedroomLamp || light = LeftBedroomLamp)
|> Seq.toList
member this.Label = member this.Label =
this.LightsThatAreOn this.LightsThatAreOn
|> Seq.map (fun (light, state) -> $"{(lightProps light).FriendlyName.Get}: {state}") |> Seq.map (fun (light, state) -> $"{(lightProps light).FriendlyName.Get}: {state}")

View file

@ -36,7 +36,11 @@ let private genHumanInteraction =
|> Gen.map Interaction.HumanInteraction |> Gen.map Interaction.HumanInteraction
let private genRemoteInteraction = let private genRemoteInteraction =
Gen.elements [ RemotePressedOnButton; RemotePressedOffButton; RemotePressedLeftButton ] Gen.elements
[ RemotePressedOnButton
RemotePressedOffButton
RemotePressedLeftButton
RemotePressedRightButton ]
|> Gen.map RemoteInteraction |> Gen.map RemoteInteraction
let private genInteraction = let private genInteraction =

View file

@ -82,55 +82,59 @@ type NightLightTests() =
|> Prop.trivial (fakeHome.LightsThatAreOn.Length = 0) |> Prop.trivial (fakeHome.LightsThatAreOn.Length = 0)
[<Property(Arbitrary = [| typeof<ArbitraryInteractions> |])>] [<Property(Arbitrary = [| typeof<ArbitraryInteractions> |])>]
let ``All non-remotely controlled lights with power should be on`` (interactions: Interaction list) = let ``All lights with power should have the correct state`` (interactions: Interaction list) =
let fakeHome = createFakeHomeWithNightLightAndInteract interactions let fakeHome = createFakeHomeWithNightLightAndInteract interactions
let nonRemotelyControlledLightsWithPower = let lightsWithPower =
fakeHome.NonRemotelyControlledLightStates fakeHome.LightStates
|> Seq.filter (fun (light, _) -> doesLightHavePowerAfterInteractions light interactions) |> Seq.filter (fun (light, _) -> doesLightHavePowerAfterInteractions light interactions)
|> Seq.toList |> Seq.toList
nonRemotelyControlledLightsWithPower let lastBedroomRemoteInteraction =
|> Seq.forall (snd >> _.IsOn) interactions
|> Prop.collect $"{nonRemotelyControlledLightsWithPower.Length} non-remotely controlled light(s) with power" |> Seq.indexed
|> Prop.label fakeHome.Label |> Seq.choose (fun (index, interaction) ->
|> Prop.trivial (nonRemotelyControlledLightsWithPower.Length = 0) match interaction with
| Interaction.RemoteInteraction remoteInteraction ->
match remoteInteraction with
| RemotePressedOnButton
| RemotePressedOffButton
| RemotePressedLeftButton -> Some(index, remoteInteraction)
| RemotePressedRightButton -> None
| _ -> None)
|> Seq.tryLast
[<Property(Arbitrary = [| typeof<ArbitraryInteractions> |])>] let newDayStartedSinceBedroomRemote =
let ``All remotely-controlled lights with power should have the correct state`` (interactions: Interaction list) = hasNewDayStartedSince interactions lastBedroomRemoteInteraction
let fakeHome = createFakeHomeWithNightLightAndInteract interactions
let remotelyControlledLightsWithPower = let hasPressedRight =
fakeHome.RemotelyControlledLightStates interactions
|> Seq.filter (fun (light, _) -> doesLightHavePowerAfterInteractions light interactions) |> Seq.exists (function
|> Seq.toList | Interaction.RemoteInteraction RemotePressedRightButton -> true
| _ -> false)
let allOn (ls: (Light * LightState) seq) = ls |> Seq.forall (snd >> _.IsOn) let isExpectedOn light =
let allOff (ls: (Light * LightState) seq) = ls |> Seq.forall (snd >> _.IsOff) match light with
| LeftBedroomLamp
let controlledByLeft ls = | RightBedroomLamp ->
ls |> Seq.filter (fun (light, _) -> light = LeftBedroomLamp) if newDayStartedSinceBedroomRemote then
true
let controlledByRight ls =
ls |> Seq.filter (fun (light, _) -> light = RightBedroomLamp)
let maybeLastRemoteInteraction = tryGetLastRemoteInteraction interactions
let hasNewDayStartedSinceThen =
hasNewDayStartedSince interactions maybeLastRemoteInteraction
if hasNewDayStartedSinceThen then
remotelyControlledLightsWithPower |> allOn
else else
match maybeLastRemoteInteraction with match lastBedroomRemoteInteraction with
| Some(_, RemotePressedOnButton) -> remotelyControlledLightsWithPower |> allOn | Some(_, RemotePressedOffButton) -> false
| Some(_, RemotePressedOffButton) -> remotelyControlledLightsWithPower |> allOff | Some(_, RemotePressedLeftButton) -> light = LeftBedroomLamp
| Some(_, RemotePressedLeftButton) -> | Some(_, RemotePressedOnButton) -> true
remotelyControlledLightsWithPower |> controlledByLeft |> allOn | Some(_, RemotePressedRightButton) -> failwith "unexpected"
&& remotelyControlledLightsWithPower |> controlledByRight |> allOff | None -> true
| None -> remotelyControlledLightsWithPower |> allOn | LivingRoomWallLamp
|> Prop.collect $"last remote interaction is {maybeLastRemoteInteraction |> Option.map snd}" | LivingRoomFloorLamp -> not hasPressedRight
|> Prop.collect $"{remotelyControlledLightsWithPower.Length} remotely controlled light(s) with power" | BathroomCeilingLamp -> true
|> Prop.classify hasNewDayStartedSinceThen "new day has started since then"
lightsWithPower
|> Seq.forall (fun (light, state) -> state.IsOn = isExpectedOn light)
|> Prop.collect $"last bedroom remote interaction is {lastBedroomRemoteInteraction |> Option.map snd}"
|> Prop.collect $"pressed right: {hasPressedRight}"
|> Prop.collect $"{lightsWithPower.Length} light(s) with power"
|> Prop.classify newDayStartedSinceBedroomRemote "new day since bedroom remote"
|> Prop.label fakeHome.Label |> Prop.label fakeHome.Label
|> Prop.trivial (remotelyControlledLightsWithPower.Length = 0) |> Prop.trivial (lightsWithPower.Length = 0)

View file

@ -52,7 +52,11 @@ let internal createOrUpdateNightLightState
brightness.Scale(getAlarmWeight time) brightness.Scale(getAlarmWeight time)
else else
brightness brightness
State = if alarm then On else previousState }) State =
if alarm && (light = RightBedroomLamp || light = LeftBedroomLamp) then
On
else
previousState })
|> Map.ofSeq |> Map.ofSeq
{ Time = time { Time = time
@ -117,6 +121,11 @@ type NightLightStateMachine private (maybeState: NightLightState option) =
|> withAlarmOff |> withAlarmOff
|> withStateFor RightBedroomLamp Off |> withStateFor RightBedroomLamp Off
|> withStateFor LeftBedroomLamp On |> withStateFor LeftBedroomLamp On
| PressedRight ->
currentState
|> withAlarmOff
|> withStateFor LivingRoomWallLamp Off
|> withStateFor LivingRoomFloorLamp Off
NightLightStateMachine(Some newNightLightState), NightLightStateMachine(Some newNightLightState),
generateZigbeeCommandsForDifference (Some currentState) newNightLightState generateZigbeeCommandsForDifference (Some currentState) newNightLightState

View file

@ -8,6 +8,7 @@ type Action =
| PressedOn | PressedOn
| PressedOff | PressedOff
| PressedLeft | PressedLeft
| PressedRight
type ZigbeeEvent = type ZigbeeEvent =
| DeviceAnnounce of DeviceFriendlyName | DeviceAnnounce of DeviceFriendlyName
@ -37,6 +38,7 @@ let parseZigbeeEvent (message: Message) =
| Some(JsonValue.String "on") -> Ok(ButtonPress PressedOn) | Some(JsonValue.String "on") -> Ok(ButtonPress PressedOn)
| Some(JsonValue.String "off") -> Ok(ButtonPress PressedOff) | Some(JsonValue.String "off") -> Ok(ButtonPress PressedOff)
| Some(JsonValue.String "arrow_left_click") -> Ok(ButtonPress PressedLeft) | Some(JsonValue.String "arrow_left_click") -> Ok(ButtonPress PressedLeft)
| Some(JsonValue.String "arrow_right_click") -> Ok(ButtonPress PressedRight)
| Some _ -> Error InvalidActionField | Some _ -> Error InvalidActionField
| None -> Error MissingActionField | None -> Error MissingActionField
| _ -> return! Error <| UnknownTopic message.Topic | _ -> return! Error <| UnknownTopic message.Topic