Give every light its own type

This commit is contained in:
Sven van Heugten 2026-02-27 17:49:20 +01:00
parent dbb0389e61
commit 50c8a413a6
6 changed files with 72 additions and 50 deletions

View file

@ -50,7 +50,7 @@ type FakeLight(light: Light) =
if hasPower then
brightness <- newBrightness
if light.Bulb = IkeaBulb then
if (lightProps light).Bulb = IkeaBulb then
state <- true
member _.SetColor(newColor: Color) =
@ -60,7 +60,7 @@ type FakeLight(light: Light) =
type FakeHome() =
let friendlyNameToFakeLight =
lights
|> Seq.map (fun light -> light.FriendlyName, FakeLight light)
|> Seq.map (fun light -> (lightProps light).FriendlyName, FakeLight light)
|> Map.ofSeq
let onEventPublished = new Event<Event>()
@ -113,17 +113,17 @@ type FakeHome() =
member _.Interact(interaction: Interaction) =
match interaction with
| HumanInteraction(LightPoweredOn light) ->
friendlyNameToFakeLight[light.FriendlyName].PowerOn()
friendlyNameToFakeLight[(lightProps light).FriendlyName].PowerOn()
{ Topic = "zigbee2mqtt/bridge/event"
Payload =
$@"{{
""type"": ""device_announce"",
""data"": {{ ""friendly_name"": ""{light.FriendlyName.Get}"" }}
""data"": {{ ""friendly_name"": ""{(lightProps light).FriendlyName.Get}"" }}
}}" }
|> ReceivedZigbeeEvent
|> onEventPublished.Trigger
| HumanInteraction(LightPoweredOff light) -> friendlyNameToFakeLight[light.FriendlyName].PowerOff()
| HumanInteraction(LightPoweredOff light) -> friendlyNameToFakeLight[(lightProps light).FriendlyName].PowerOff()
| RemoteInteraction RemotePressedOnButton ->
{ Topic = $"zigbee2mqtt/{remoteControlFriendlyName.Get}"
Payload = @"{ ""action"": ""on"" }" }
@ -149,15 +149,15 @@ type FakeHome with
member this.NonRemotelyControlledLightStates =
this.LightStates
|> Seq.filter (fst >> _.ControlledWithRemote >> (=) NonRemote)
|> Seq.filter (fst >> lightProps >> _.ControlledWithRemote >> (=) NonRemote)
|> Seq.toList
member this.RemotelyControlledLightStates =
this.LightStates
|> Seq.filter (fst >> _.ControlledWithRemote >> (<>) NonRemote)
|> Seq.filter (fst >> lightProps >> _.ControlledWithRemote >> (<>) NonRemote)
|> Seq.toList
member this.Label =
this.LightsThatAreOn
|> Seq.map (fun (light, state) -> $"{light.FriendlyName.Get}: {state}")
|> Seq.map (fun (light, state) -> $"{(lightProps light).FriendlyName.Get}: {state}")
|> String.concat ", "

View file

@ -9,27 +9,27 @@ type ArbitraryLight =
type ArbitraryNonRemotelyControlledLight =
static member Light() =
lights
|> Seq.filter _.ControlledWithRemote.IsNonRemote
|> Seq.filter (fun light -> (lightProps light).ControlledWithRemote.IsNonRemote)
|> Gen.elements
|> Arb.fromGen
type ArbitraryLeftRemotelyControlledLight =
static member Light() =
lights
|> Seq.filter _.ControlledWithRemote.IsRemoteLeft
|> Seq.filter (fun light -> (lightProps light).ControlledWithRemote.IsRemoteLeft)
|> Gen.elements
|> Arb.fromGen
type ArbitraryRightRemotelyControlledLight =
static member Light() =
lights
|> Seq.filter _.ControlledWithRemote.IsRemoteRight
|> Seq.filter (fun light -> (lightProps light).ControlledWithRemote.IsRemoteRight)
|> Gen.elements
|> Arb.fromGen
type ArbitraryRemotelyControlledLight =
static member Light() =
lights
|> Seq.filter (not << _.ControlledWithRemote.IsNonRemote)
|> Seq.filter (fun light -> (lightProps light).ControlledWithRemote.IsNonRemote |> not)
|> Gen.elements
|> Arb.fromGen

View file

@ -52,7 +52,7 @@ type NightLightTests() =
&& time <= endOfAlarm
let scaledForAlarm light brightness =
if light.ControlledWithRemote <> NonRemote && alarm then
if (lightProps light).ControlledWithRemote <> NonRemote && alarm then
float brightness * ((time - startOfDay) / (endOfAlarm - startOfDay)) |> byte
else
brightness
@ -60,14 +60,14 @@ type NightLightTests() =
fakeHome.LightStates
|> Seq.forall (fun (light, state) ->
let maybeExpectedBrightness =
match light, state with
match (lightProps light).Bulb, state with
| _, Off -> None
| { Bulb = IkeaBulb }, On(_, White) -> Some 254uy
| { Bulb = IkeaBulb }, On(_, Yellow) -> Some 210uy
| { Bulb = IkeaBulb }, On(_, Red) -> Some 254uy
| { Bulb = PaulmannBulb }, On(_, White) -> Some 35uy
| { Bulb = PaulmannBulb }, On(_, Yellow) -> Some 35uy
| { Bulb = PaulmannBulb }, On(_, Red) -> Some 80uy
| IkeaBulb, On(_, White) -> Some 254uy
| IkeaBulb, On(_, Yellow) -> Some 210uy
| IkeaBulb, On(_, Red) -> Some 254uy
| PaulmannBulb, On(_, White) -> Some 35uy
| PaulmannBulb, On(_, Yellow) -> Some 35uy
| PaulmannBulb, On(_, Red) -> Some 80uy
|> Option.map (scaledForAlarm light)
let maybeActualBrightness =
@ -109,7 +109,7 @@ type NightLightTests() =
let allOff (ls: (Light * LightState) seq) = ls |> Seq.forall (snd >> _.IsOff)
let controlledBy remote ls =
ls |> Seq.filter (fst >> _.ControlledWithRemote >> (=) remote)
ls |> Seq.filter (fst >> lightProps >> _.ControlledWithRemote >> (=) remote)
let maybeLastRemoteInteraction = tryGetLastRemoteInteraction interactions

View file

@ -46,32 +46,52 @@ type LightControl =
| RemoteRight
type Light =
| VardagsrumFonsterlampa
| VardagsrumVagglampa
| VardagsrumGolvlampa
| BadrumTaklampa
| SovrumNattduksbordlampa
type LightProps =
{ FriendlyName: DeviceFriendlyName
Room: Room
Bulb: Bulb
ControlledWithRemote: LightControl }
let lights =
[ { FriendlyName = DeviceFriendlyName "Vardagsrum - Fönsterlampa"
let lightProps light =
match light with
| VardagsrumFonsterlampa ->
{ FriendlyName = DeviceFriendlyName "Vardagsrum - Fönsterlampa"
Room = Bedroom
Bulb = IkeaBulb
ControlledWithRemote = RemoteRight }
| VardagsrumVagglampa ->
{ FriendlyName = DeviceFriendlyName "Vardagsrum - Vägglampa"
Room = LivingRoom
Bulb = PaulmannBulb
ControlledWithRemote = NonRemote }
| VardagsrumGolvlampa ->
{ FriendlyName = DeviceFriendlyName "Vardagsrum - Golvlampa"
Room = LivingRoom
Bulb = PaulmannBulb
ControlledWithRemote = NonRemote }
| BadrumTaklampa ->
{ FriendlyName = DeviceFriendlyName "Badrum - Taklampa"
Room = Bathroom
Bulb = IkeaBulb
ControlledWithRemote = NonRemote }
| SovrumNattduksbordlampa ->
{ FriendlyName = DeviceFriendlyName "Sovrum - Nattduksbordlampa"
Room = Bedroom
Bulb = IkeaBulb
ControlledWithRemote = RemoteLeft } ]
ControlledWithRemote = RemoteLeft }
let lights =
[ VardagsrumFonsterlampa
VardagsrumVagglampa
VardagsrumGolvlampa
BadrumTaklampa
SovrumNattduksbordlampa ]
let remoteControlFriendlyName = DeviceFriendlyName "Fjärrkontroll"

View file

@ -9,14 +9,14 @@ open NightLight.Core.Moods
open FsToolkit.ErrorHandling
let internal tryFindLight friendlyName =
Seq.tryFind (fun light -> light.FriendlyName = friendlyName) lights
Seq.tryFind (fun light -> (lightProps light).FriendlyName = friendlyName) lights
let internal generateZigbeeCommandsToFixLight (light: Light) (desiredLightState: LightState) =
seq {
match light.ControlledWithRemote, desiredLightState.State with
match (lightProps light).ControlledWithRemote, desiredLightState.State with
| NonRemote, On -> ()
| NonRemote, Off -> failwith $"Unexpectly trying to turn off {light}. It's not remote-controlled."
| _, On when light.Bulb = IkeaBulb -> () // Rely on the brightness command for turning it on
| _, On when (lightProps light).Bulb = IkeaBulb -> () // Rely on the brightness command for turning it on
| _, _ -> yield generateStateCommand desiredLightState.State light
if desiredLightState.State = On then
@ -40,7 +40,8 @@ let internal createOrUpdateNightLightState
lights
|> Seq.map (fun light ->
let color, brightness =
getDesiredMood light.Room partOfDay |> getDesiredColorAndBrightness light.Bulb
getDesiredMood (lightProps light).Room partOfDay
|> getDesiredColorAndBrightness (lightProps light).Bulb
let previousState =
maybeOldLightToState
@ -50,7 +51,7 @@ let internal createOrUpdateNightLightState
light,
{ Color = color
Brightness =
if alarm && light.ControlledWithRemote <> NonRemote then
if alarm && (lightProps light).ControlledWithRemote <> NonRemote then
brightness.Scale(getAlarmWeight time)
else
brightness
@ -72,7 +73,7 @@ let internal withStateFor (light: Light) (state: State) (oldNightLightState: Nig
let internal withStateForRemoteControlledLights (state: State) (oldNightLightState: NightLightState) =
lights
|> Seq.filter (not << _.ControlledWithRemote.IsNonRemote)
|> Seq.filter (fun light -> (lightProps light).ControlledWithRemote.IsNonRemote |> not)
|> Seq.fold (fun acc light -> acc |> withStateFor light state) oldNightLightState
let internal withAlarmOff (oldNightLightState: NightLightState) =
@ -113,7 +114,8 @@ type NightLightStateMachine private (maybeState: NightLightState option) =
| PressedOff -> currentState |> withAlarmOff |> withStateForRemoteControlledLights Off
| PressedLeft ->
let lightThatShouldBeOn =
lights |> Seq.find (fun light -> light.ControlledWithRemote = RemoteLeft)
lights
|> Seq.find (fun light -> (lightProps light).ControlledWithRemote = RemoteLeft)
currentState
|> withAlarmOff

View file

@ -4,7 +4,7 @@ open System.Text.Json.Nodes
open NightLight.Core.Models
let toZigbeeCommand light payload =
let topic = $"zigbee2mqtt/{light.FriendlyName.Get}/set"
let topic = $"zigbee2mqtt/{(lightProps light).FriendlyName.Get}/set"
{ Topic = topic; Payload = payload }
let generateStateCommand state light =
@ -15,7 +15,7 @@ let generateStateCommand state light =
| On -> "ON"
| Off -> "OFF"
if light.Bulb = IkeaBulb then
if (lightProps light).Bulb = IkeaBulb then
commandObj["transition"] <- 0
commandObj.ToJsonString() |> toZigbeeCommand light