Test the bedroom spec

This commit is contained in:
2026-08-25 12:41:42 +03:00
parent 7051eaf244
commit 771d702abd
6 changed files with 206 additions and 3 deletions
+4 -1
View File
@@ -62,6 +62,7 @@ library
exposed-modules: AFRP exposed-modules: AFRP
, HomeAssistant.Controller , HomeAssistant.Controller
, HomeAssistant.Controller.Bedroom , HomeAssistant.Controller.Bedroom
, HomeAssistant.Controller.Children
, HomeAssistant.Controller.Ruuvi , HomeAssistant.Controller.Ruuvi
, HomeAssistant.Runtime , HomeAssistant.Runtime
, HomeAssistant.Runtime.Bus , HomeAssistant.Runtime.Bus
@@ -130,11 +131,13 @@ test-suite home-assistant-controller-test
-- Modules included in this executable, other than Main. -- Modules included in this executable, other than Main.
other-modules: AFRPSpec other-modules: AFRPSpec
, BackoffProp
, BedroomSpec
, BusSpec , BusSpec
, ConnectionSpec , ConnectionSpec
, RuntimeSpec , RuntimeSpec
, SupervisorSpec , SupervisorSpec
, BackoffProp , Support
-- LANGUAGE extensions used by modules in this package. -- LANGUAGE extensions used by modules in this package.
-- other-extensions: -- other-extensions:
+14
View File
@@ -0,0 +1,14 @@
module HomeAssistant.Controller.Children where
import HomeAssistant.Controller (HASS)
import AFRP (Event)
import Data.Aeson (Value)
import Control.Arrow (Arrow(..))
-- I don't have any proper presence sensors in their bedroom
-- and they are notoriously bad at changing clothes in complete darkness
-- So I have set up an automation that attempts to turn on the lights sometime
-- before they leave for school and turns them off a bit later
--
-- I don't have enough primitives for this yet, leaving as a placeholder
schoolLightController :: HASS (Event Value) ()
schoolLightController = arr (const ())
+2 -2
View File
@@ -46,8 +46,8 @@ controllers =
[ Controller "bedroom-presence" bedroomPresenceController False [ Controller "bedroom-presence" bedroomPresenceController False
, Controller "bedroom-button" bedroomButtonController False -- This works but leaving for vacation , Controller "bedroom-button" bedroomButtonController False -- This works but leaving for vacation
, Controller "bedroom-drawer" bedroomDrawerController True , Controller "bedroom-drawer" bedroomDrawerController True
, Controller "bedroom-humidifier" humidifierController True , Controller "bedroom-humidifier" humidifierController False
, Controller "ruuvi-controller" ruuviController True , Controller "ruuvi-controller" ruuviController False
] ]
-- | Steps the machine for every inbound message; service calls go to the -- | Steps the machine for every inbound message; service calls go to the
+103
View File
@@ -0,0 +1,103 @@
{-# LANGUAGE OverloadedStrings #-}
module BedroomSpec (spec) where
import AFRP (Event (..))
import qualified Data.Text as T
import Data.Aeson (Value)
import HomeAssistant.Controller
import HomeAssistant.Controller.Bedroom
import Support
import Test.Hspec
masseButton :: T.Text -> Event Value
masseButton = Event . buttonEvent "event.bedroom_quick_remote_masse_action"
enishenButton :: T.Text -> Event Value
enishenButton = Event . buttonEvent "event.bedroom_quick_jemina_action"
drawerState :: T.Text -> Event Value
drawerState = Event . stateEvent "binary_sensor.bedroom_nightstand_drawer_sensor_masse_contact"
spec :: Spec
spec = describe "Bedroom" $ do
drawerSpec
buttonSpec
drawerSpec :: Spec
drawerSpec = describe "bedroomDrawerController" $ do
let entity = EntityId "switch.bedroom_drawer_light_masse"
it "turns the drawer light on when the drawer opens" $
services (runHASS bedroomDrawerController [drawerState "on"])
`shouldBe` [[switch [entity] True]]
it "turns the drawer light off when the drawer closes" $
services (runHASS bedroomDrawerController [drawerState "off"])
`shouldBe` [[switch [entity] False]]
it "toggles the light as the drawer opens and closes" $
services (runHASS bedroomDrawerController
[ drawerState "on", drawerState "off", drawerState "on" ])
`shouldBe` [ [switch [entity] True]
, [switch [entity] False]
, [switch [entity] True]
]
it "ignores state events for other entities" $
services (runHASS bedroomDrawerController
[Event (stateEvent "binary_sensor.some_other_contact" "on")])
`shouldBe` [[]]
it "does nothing on Tick" $
services (runHASS bedroomDrawerController [Tick])
`shouldBe` [[]]
buttonSpec :: Spec
buttonSpec = describe "bedroomButtonController" $ do
let area = AreaId "makuuhuone"
it "Masse single click turns on his nightstand scene (lowest)" $
services (runHASS bedroomButtonController [masseButton "1_short_release"])
`shouldBe` [[activateScene "scene.makuuhuone_masse"]]
it "Masse double click turns on the middle scene" $
services (runHASS bedroomButtonController [masseButton "1_double_press"])
`shouldBe` [[activateScene "scene.makuuhuone_keski"]]
it "Masse long click turns on the bright scene" $
services (runHASS bedroomButtonController [masseButton "1_long_press"])
`shouldBe` [[activateScene "scene.makuuhuone_kirkas"]]
it "Masse off click turns the bedroom lights off" $
services (runHASS bedroomButtonController [masseButton "2_short_release"])
`shouldBe` [[light [area] False]]
it "Enishen single click turns on her nightstand scene (lowest)" $
services (runHASS bedroomButtonController [enishenButton "1_short_release"])
`shouldBe` [[activateScene "scene.makuuhuone_jemina"]]
it "Enishen double click turns on the middle scene" $
services (runHASS bedroomButtonController [enishenButton "1_double_press"])
`shouldBe` [[activateScene "scene.makuuhuone_keski"]]
it "Enishen long click turns on the bright scene" $
services (runHASS bedroomButtonController [enishenButton "1_long_press"])
`shouldBe` [[activateScene "scene.makuuhuone_kirkas"]]
it "Enishen off click turns the bedroom lights off" $
services (runHASS bedroomButtonController [enishenButton "2_short_release"])
`shouldBe` [[light [area] False]]
it "ignores the initial press (scene only fires on release)" $
services (runHASS bedroomButtonController [masseButton "1_initial_press"])
`shouldBe` [[]]
it "ignores button events for other entities" $
services (runHASS bedroomButtonController
[Event (buttonEvent "event.some_other_action" "1_short_release")])
`shouldBe` [[]]
it "does nothing on Tick" $
services (runHASS bedroomButtonController [Tick])
`shouldBe` [[]]
+2
View File
@@ -3,6 +3,7 @@ module Main (main) where
import Test.Hspec (hspec) import Test.Hspec (hspec)
import qualified AFRPSpec import qualified AFRPSpec
import qualified BackoffProp import qualified BackoffProp
import qualified BedroomSpec
import qualified BusSpec import qualified BusSpec
import qualified ConnectionSpec import qualified ConnectionSpec
import qualified RuntimeSpec import qualified RuntimeSpec
@@ -11,6 +12,7 @@ import qualified SupervisorSpec
main :: IO () main :: IO ()
main = hspec $ do main = hspec $ do
AFRPSpec.spec AFRPSpec.spec
BedroomSpec.spec
BusSpec.spec BusSpec.spec
ConnectionSpec.spec ConnectionSpec.spec
RuntimeSpec.spec RuntimeSpec.spec
+81
View File
@@ -0,0 +1,81 @@
{-# LANGUAGE OverloadedStrings #-}
module Support
( Acc(..)
, interp
, runHASS
, services
, fakeRequest
, sec
, stateEvent
, buttonEvent
) where
import Control.Monad.Fix (MonadFix (..))
import Data.Aeson (Value, object, (.=))
import qualified Data.Text as T
import Data.Time (UTCTime (..))
import Data.UUID (nil)
import AFRP (Mealy (..), Request (..))
import HomeAssistant.Controller (HASSEff (..), Service)
fakeRequest :: Request
fakeRequest = Request (sec 0) nil
sec :: Integer -> UTCTime
sec n = UTCTime (toEnum 0) (fromIntegral n)
-- | A pure Writer-like monad accumulating `Service` calls per step.
newtype Acc a = Acc { runAcc :: [Service] -> (a, [Service]) }
instance Functor Acc where
fmap f (Acc g) = Acc $ \s -> let (a, s') = g s in (f a, s')
instance Applicative Acc where
pure a = Acc (\s -> (a, s))
Acc f <*> Acc x = Acc $ \s -> let (f', s') = f s; (a, s'') = x s' in (f' a, s'')
instance Monad Acc where
Acc m >>= k = Acc $ \s -> let (a, s') = m s; (b, s'') = runAcc (k a) s' in (b, s'')
instance MonadFix Acc where
mfix f = Acc $ \s -> let (a, s') = runAcc (f a) s in (a, s')
-- | Interpret `HASSEff` in `Acc`: record `CallService`, drop tracing/debug.
interp :: HASSEff a -> Acc a
interp (CallService _ svc) = Acc $ \s -> ((), s ++ [svc])
interp (Debug _) = pure ()
interp (Trace _ _) = pure ()
-- | Run a HASS arrow over a list of inputs, collecting per-step emitted services.
runHASS :: Mealy HASSEff a b -> [a] -> [(b, [Service])]
runHASS _ [] = []
runHASS m (a : as) =
case runAcc (runMealy m interp fakeRequest a) [] of
((b, m'), svcs) -> (b, svcs) : runHASS m' as
services :: [(b, [Service])] -> [[Service]]
services = map snd
-- | Build a state-change event payload matching `entityChangeEvent'` / `entityBool'` lenses.
stateEvent :: T.Text -> T.Text -> Value
stateEvent entityId state = object
[ "event" .= object
[ "data" .= object
[ "entity_id" .= entityId
, "new_state" .= object [ "state" .= state ]
]
]
]
-- | Build an Ikea button event payload matching `ikeaQuickButton` lenses.
buttonEvent :: T.Text -> T.Text -> Value
buttonEvent entityId eventType = object
[ "event" .= object
[ "data" .= object
[ "entity_id" .= entityId
, "new_state" .= object
[ "attributes" .= object [ "event_type" .= eventType ] ]
]
]
]