A lot of internals as I tried to do a switch based logic
This commit is contained in:
+51
-1
@@ -1,8 +1,10 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE Arrows #-}
|
||||
|
||||
module AFRP
|
||||
( Mealy(..)
|
||||
, eff
|
||||
, withEntities
|
||||
, Event(..)
|
||||
, hold
|
||||
, events
|
||||
@@ -20,6 +22,7 @@ module AFRP
|
||||
, lMerge
|
||||
, Request(..)
|
||||
, edge
|
||||
, dropFirst
|
||||
, duration
|
||||
, tag
|
||||
, isEvent
|
||||
@@ -29,12 +32,14 @@ module AFRP
|
||||
, sliding
|
||||
, fixed
|
||||
, debounce
|
||||
, currentTime
|
||||
, onEvent
|
||||
) where
|
||||
|
||||
import Control.Category (Category(..), (>>>))
|
||||
import Prelude hiding ((.), id)
|
||||
import Control.Arrow (Arrow(..), ArrowChoice(..), ArrowLoop(..))
|
||||
import Data.Time (UTCTime, NominalDiffTime, diffUTCTime, addUTCTime)
|
||||
import Data.Time (UTCTime, NominalDiffTime, diffUTCTime, addUTCTime, TimeZone, LocalTime, utcToLocalTime)
|
||||
import Control.Monad.Fix (MonadFix (mfix))
|
||||
import Data.Either (fromLeft)
|
||||
import Data.Bool (bool)
|
||||
@@ -45,6 +50,7 @@ import qualified Data.Text as T
|
||||
|
||||
data Request = Request
|
||||
{ requestTime :: !UTCTime
|
||||
, requestTimeZone :: !TimeZone
|
||||
, requestTraceId :: !UUID
|
||||
} deriving Show
|
||||
|
||||
@@ -56,10 +62,27 @@ data Mealy eff a b = Mealy
|
||||
, runMealy :: forall m. MonadFix m => (forall x. eff x -> m x) -> Request -> a -> m (b, Mealy eff a b)
|
||||
}
|
||||
|
||||
|
||||
instance Semigroup b => Semigroup (Mealy eff a b) where
|
||||
Mealy ast af <> Mealy bst bf = Mealy (ast <> bst) $ \nt r a -> do
|
||||
(x, af') <- af nt r a
|
||||
(x', bf') <- bf nt r a
|
||||
pure (x <> x', af' <> bf')
|
||||
|
||||
|
||||
instance Monoid b => Monoid (Mealy eff a b) where
|
||||
mempty = Mealy mempty $ \_ _ _ -> pure (mempty, mempty)
|
||||
|
||||
eff :: (Request -> a -> eff b) -> Mealy eff a b
|
||||
eff f = Mealy mempty $ \nt req x ->
|
||||
nt (f req x) >>= \b -> pure (b, eff f)
|
||||
|
||||
-- | Override the static entity set of an arrow. Use when a combinator
|
||||
-- (e.g. 'switch') hides continuation entities from the runtime's
|
||||
-- startup subscription scan.
|
||||
withEntities :: S.Set T.Text -> Mealy eff a b -> Mealy eff a b
|
||||
withEntities es (Mealy _ f) = Mealy es f
|
||||
|
||||
instance Category (Mealy eff) where
|
||||
id = Mealy mempty (\_ _ x -> pure (x, id))
|
||||
(Mealy ast f) . (Mealy bst g) = Mealy (ast <> bst) $ \nt t a -> do
|
||||
@@ -102,6 +125,12 @@ data Event a
|
||||
| Event a
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
instance Semigroup (Event a) where
|
||||
(<>) = lMerge
|
||||
|
||||
instance Monoid (Event a) where
|
||||
mempty = Tick
|
||||
|
||||
hold :: a -> Mealy eff (Event a) a
|
||||
hold a = Mealy mempty $ \_ _ -> \case
|
||||
Tick -> pure (a, hold a)
|
||||
@@ -243,6 +272,18 @@ edge = go False
|
||||
False -> pure (Tick, go False)
|
||||
|
||||
|
||||
-- | Drop the first 'Event' and pass through everything after. Useful for
|
||||
-- ignoring a self-triggered event (e.g. a service call that changes the
|
||||
-- very entity the arrow listens to).
|
||||
dropFirst :: Mealy eff (Event a) (Event a)
|
||||
dropFirst = go False
|
||||
where
|
||||
go seen = Mealy mempty $ \_ _ input ->
|
||||
case input of
|
||||
Event _ | not seen -> pure (Tick, go True)
|
||||
_ -> pure (input, go seen)
|
||||
|
||||
|
||||
duration :: forall eff a. Mealy eff a NominalDiffTime
|
||||
duration = mapAccumRequest go (Nothing @(UTCTime, NominalDiffTime)) (maybe 0 snd)
|
||||
where
|
||||
@@ -295,3 +336,12 @@ fixed seconds = mapAccumRequest go Nothing (maybe [] ((`appEndo` []) . snd))
|
||||
| otherwise -> Just (end, acc)
|
||||
Event a | requestTime req >= end -> Just (addUTCTime (fromIntegral seconds) end, e a)
|
||||
| otherwise -> Just (end, acc <> e a)
|
||||
|
||||
|
||||
currentTime :: Mealy eff a LocalTime
|
||||
currentTime = Mealy mempty $ \_ Request{requestTime, requestTimeZone} _ ->
|
||||
pure (utcToLocalTime requestTimeZone requestTime, currentTime)
|
||||
|
||||
|
||||
onEvent :: Mealy eff a () -> Mealy eff (Event a) ()
|
||||
onEvent f = events >>> (arr (const ()) ||| f)
|
||||
|
||||
@@ -23,16 +23,18 @@ module HomeAssistant.Controller
|
||||
, traceValue
|
||||
, switch
|
||||
, Target(..)
|
||||
, brightness
|
||||
, Light(..)
|
||||
) where
|
||||
|
||||
import AFRP (Mealy (..), eff, Event(..), events, filterA, (>>|), toEvent, Request)
|
||||
import Control.Arrow (Arrow(..), returnA)
|
||||
import Control.Category ((>>>))
|
||||
import Data.Aeson (Value)
|
||||
import Data.Aeson (Value, object, (.=))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Set as S
|
||||
import Control.Lens (has, only, (^?), to)
|
||||
import Data.Aeson.Lens (key, _String)
|
||||
import Data.Aeson.Lens (key, _String, _Integral)
|
||||
import qualified Data.Text.Lens as TL
|
||||
import Data.Bool (bool)
|
||||
|
||||
@@ -84,12 +86,21 @@ presence entityId =entityBool entityId
|
||||
|
||||
|
||||
|
||||
data Light
|
||||
= Off
|
||||
| On { brightnessPercentage :: Maybe Double }
|
||||
|
||||
-- Turn off lights when door is closed
|
||||
light :: [Target] -> Bool -> Service
|
||||
light targets b = Service
|
||||
light :: [Target] -> Light -> Service
|
||||
light targets (On {brightnessPercentage}) = Service
|
||||
{ serviceDomain="light"
|
||||
, serviceName= bool "turn_off" "turn_on" b
|
||||
, serviceName= "turn_on"
|
||||
, serviceData=fmap (\pct -> object ["brightness_pct" .= pct]) brightnessPercentage
|
||||
, serviceTarget=targets
|
||||
}
|
||||
light targets Off = Service
|
||||
{ serviceDomain="light"
|
||||
, serviceName= "turn_off"
|
||||
, serviceData=Nothing
|
||||
, serviceTarget=targets
|
||||
}
|
||||
@@ -132,3 +143,11 @@ entityRead entityId = entityRead' entityId >>> toEvent
|
||||
|
||||
entityBool :: T.Text -> Mealy eff (Event Value) (Event Bool)
|
||||
entityBool entityId = entityBool' entityId >>> toEvent
|
||||
|
||||
brightness :: T.Text -> HASS (Event Value) (Event Int)
|
||||
brightness entityId =
|
||||
entityChangeEvent' entityId
|
||||
>>| arr (maybe (Left ()) Right . eventBrightness)
|
||||
>>> AFRP.toEvent
|
||||
where
|
||||
eventBrightness v = v ^? key "event" . key "variables" . key "trigger" . key "to_state" . key "attributes" . key "brightness" . _Integral
|
||||
|
||||
@@ -46,7 +46,7 @@ bedroomPresenceController :: HASS (Event Value) ()
|
||||
bedroomPresenceController = proc x -> do
|
||||
p <- bedroomPresence -< x
|
||||
case p of
|
||||
Event Unoccupied -> callService createBedroomScene >>> callService (light bedroomLights False) -< ()
|
||||
Event Unoccupied -> callService createBedroomScene >>> callService (light bedroomLights Off) -< ()
|
||||
Event Occupied -> callService (activateScene "makuuhuone_lights_snapshot") -< ()
|
||||
_ -> returnA -< ()
|
||||
|
||||
@@ -101,11 +101,11 @@ bedroomButtonController = proc x -> do
|
||||
Event (Masse (OnButton ShortRelease)) -> callService (activateScene "scene.makuuhuone_masse") -< ()
|
||||
Event (Masse (OnButton DoubleClick)) -> callService (activateScene "scene.makuuhuone_keski") -< ()
|
||||
Event (Masse (OnButton LongClick)) -> callService (activateScene "scene.makuuhuone_kirkas") -< ()
|
||||
Event (Masse (OffButton _)) -> callService (light [AreaId "makuuhuone"] False) -< ()
|
||||
Event (Masse (OffButton _)) -> callService (light [AreaId "makuuhuone"] Off) -< ()
|
||||
Event (Enishen (OnButton ShortRelease)) -> callService (activateScene "scene.makuuhuone_jemina") -< ()
|
||||
Event (Enishen (OnButton DoubleClick)) -> callService (activateScene "scene.makuuhuone_keski") -< ()
|
||||
Event (Enishen (OnButton LongClick)) -> callService (activateScene "scene.makuuhuone_kirkas") -< ()
|
||||
Event (Enishen (OffButton _)) -> callService (light [AreaId "makuuhuone"] False) -< ()
|
||||
Event (Enishen (OffButton _)) -> callService (light [AreaId "makuuhuone"] Off) -< ()
|
||||
_ -> returnA -< ()
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,82 @@
|
||||
{-# LANGUAGE Arrows #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module HomeAssistant.Controller.Children where
|
||||
import HomeAssistant.Controller (HASS)
|
||||
import HomeAssistant.Controller (HASS, callService, Target (AreaId), light, Light(..))
|
||||
import AFRP (Event)
|
||||
import Data.Aeson (Value)
|
||||
import Control.Arrow (Arrow(..))
|
||||
import qualified AFRP
|
||||
import Control.Arrow (Arrow(..), (>>>))
|
||||
import Data.Time (Day, TimeOfDay (..), localDay, LocalTime (..))
|
||||
import Data.Time.Calendar.OrdinalDate (WeekOfYear, mondayStartWeek)
|
||||
import Data.Functor.Contravariant (Predicate (..), (>$<))
|
||||
|
||||
-- Let's see building some reasonable interface for utctime
|
||||
dow :: Day -> (WeekOfYear, Int)
|
||||
dow = mondayStartWeek
|
||||
|
||||
|
||||
weekday :: Predicate Day
|
||||
weekday = Predicate (betweenInclusive 1 5 . snd . dow)
|
||||
where
|
||||
betweenInclusive a b c = c >= a && c <= b
|
||||
|
||||
time :: (Int, Int) -> Predicate TimeOfDay
|
||||
time (h,m) = mconcat
|
||||
[ Predicate (equals h . todHour)
|
||||
, Predicate (equals m . todMin)
|
||||
]
|
||||
where
|
||||
equals a b = a == b
|
||||
|
||||
|
||||
atTime :: Predicate LocalTime -> HASS a (Event ())
|
||||
atTime p = AFRP.currentTime
|
||||
>>> arr (getPredicate p)
|
||||
>>> AFRP.edge
|
||||
|
||||
-- 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 ())
|
||||
|
||||
-- Don't mconcat these predicates they have && behavior
|
||||
-- if you mconcat the actual arrows, they combine the behaviors of the separate branches
|
||||
-- essentially becoming || behavior
|
||||
timersOff :: [Predicate LocalTime]
|
||||
timersOff =
|
||||
[ day 1 <> at (08,15)
|
||||
, day 2 <> at (09,15)
|
||||
, day 3 <> at (08,15)
|
||||
, day 4 <> at (08,15)
|
||||
, day 5 <> at (08,15)
|
||||
, at (18,57) -- debug
|
||||
]
|
||||
where
|
||||
dayOfWeek = snd . mondayStartWeek . localDay
|
||||
at (h,m) = localTimeOfDay >$< Predicate (\TimeOfDay{todHour, todMin} -> todHour == h && todMin == m)
|
||||
day n = dayOfWeek >$< Predicate (== n)
|
||||
|
||||
timersOn :: [Predicate LocalTime]
|
||||
timersOn =
|
||||
[ day 1 <> at (07,30)
|
||||
, day 2 <> at (08,30)
|
||||
, day 3 <> at (07,30)
|
||||
, day 4 <> at (07,30)
|
||||
, day 5 <> at (07,30)
|
||||
, at (18,55) -- debug
|
||||
]
|
||||
where
|
||||
dayOfWeek = snd . mondayStartWeek . localDay
|
||||
at (h,m) = localTimeOfDay >$< Predicate (\TimeOfDay{todHour, todMin} -> todHour == h && todMin == m)
|
||||
day n = dayOfWeek >$< Predicate (== n)
|
||||
|
||||
schoolLightController :: HASS a ()
|
||||
schoolLightController = lightsOn <> lightsOff
|
||||
|
||||
|
||||
lightsOn :: HASS a ()
|
||||
lightsOn = foldMap atTime timersOn
|
||||
>>> AFRP.onEvent (callService (light [AreaId "lasten_makuuhuone"] On{brightnessPercentage = Just 100}))
|
||||
|
||||
lightsOff :: HASS a ()
|
||||
lightsOff = foldMap atTime timersOff
|
||||
>>> AFRP.onEvent (callService (light [AreaId "lasten_makuuhuone"] Off))
|
||||
|
||||
@@ -18,7 +18,7 @@ import Control.Concurrent.Async (async, waitAny)
|
||||
import Control.Concurrent.STM (atomically, dupTChan, readTChan)
|
||||
import Data.Aeson (Value)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time (getCurrentTime)
|
||||
import Data.Time (getCurrentTime, getCurrentTimeZone)
|
||||
import Data.Void (Void, absurd)
|
||||
import HomeAssistant.Controller (HASS, HASSEff (..))
|
||||
import HomeAssistant.Runtime.Bus
|
||||
@@ -33,11 +33,13 @@ import Katip (runKatipT, logF, sl, Severity (..), ls, Namespace (Namespace), run
|
||||
import Control.Monad.IO.Class (liftIO, MonadIO)
|
||||
import Control.Monad.Fix (MonadFix)
|
||||
import HomeAssistant.Controller.Ruuvi (ruuviController)
|
||||
import HomeAssistant.Controller.Children (schoolLightController)
|
||||
|
||||
step :: (MonadFix m, MonadIO m) => (forall x. eff x -> m x) -> UUID -> Mealy eff a b -> a -> m (b, Mealy eff a b)
|
||||
step nt trace (Mealy _ f) a = do
|
||||
now <- liftIO getCurrentTime
|
||||
f nt (Request now trace) a
|
||||
tz <- liftIO getCurrentTimeZone
|
||||
f nt (Request now tz trace) a
|
||||
|
||||
data Controller = forall b. Controller T.Text (HASS (Event Value) b) Bool
|
||||
|
||||
@@ -47,7 +49,8 @@ controllers =
|
||||
, Controller "bedroom-button" bedroomButtonController False -- This works but leaving for vacation
|
||||
, Controller "bedroom-drawer" bedroomDrawerController True
|
||||
, Controller "bedroom-humidifier" humidifierController False
|
||||
, Controller "ruuvi-controller" ruuviController True
|
||||
, Controller "ruuvi-controller" ruuviController False
|
||||
, Controller "school-light-controller" schoolLightController True
|
||||
]
|
||||
|
||||
-- | Steps the machine for every inbound message; service calls go to the
|
||||
@@ -62,7 +65,7 @@ runController bus (Controller name machine _enabled) = do
|
||||
msg <- atomically (readTChan inbound)
|
||||
uuid <- UUID.V4.nextRandom
|
||||
let ns = Namespace [name]
|
||||
(_, f') <- step (runKatipContextT (busLogEnv bus) () ns . channelHassEval bus) uuid f (Event msg)
|
||||
(_, f') <- step (runKatipContextT (busLogEnv bus) () ns . channelHassEval bus) uuid f msg
|
||||
go inbound f'
|
||||
|
||||
defaultMain :: IO ()
|
||||
|
||||
@@ -26,14 +26,14 @@ import Katip (LogEnv, closeScribes, mkHandleScribe, ColorStrategy (..), permitIt
|
||||
import Control.Exception (bracket)
|
||||
import System.IO (stdout)
|
||||
import Data.UUID (toText)
|
||||
import AFRP (Request(..))
|
||||
import AFRP (Request(..), Event(..))
|
||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||
|
||||
-- | Shared runtime state: inbound is a broadcast channel (controllers
|
||||
-- read from 'dupTChan' copies), outbound queues service calls for the
|
||||
-- writer, conn holds the current websocket (Nothing before first connect).
|
||||
data Bus = Bus
|
||||
{ busInbound :: TChan Value
|
||||
{ busInbound :: TChan (Event Value)
|
||||
, busOutbound :: TChan (Request, Service)
|
||||
, busConn :: TVar (Maybe Connection)
|
||||
, busGen :: CallIdGen
|
||||
|
||||
@@ -15,13 +15,14 @@ import Control.Concurrent.STM
|
||||
, writeTChan
|
||||
, writeTVar
|
||||
)
|
||||
import Control.Concurrent.Async (race)
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Exception (onException)
|
||||
import Control.Exception.Annotated (throw)
|
||||
import Control.Lens ((^?))
|
||||
import Control.Monad (forever, forM_)
|
||||
import Data.Aeson (Value, eitherDecode, encode, object, (.=))
|
||||
import Data.Aeson.Lens (key, _String)
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text as T
|
||||
import Data.Void (Void)
|
||||
@@ -31,7 +32,7 @@ import HomeAssistant.Runtime.Supervisor (Fatal (..))
|
||||
import qualified Network.WebSockets as WS
|
||||
import Katip (runKatipContextT, sl, logFM, Severity (..), ls)
|
||||
import Data.UUID (toText)
|
||||
import AFRP (Request(..))
|
||||
import AFRP (Request(..), Event(..))
|
||||
|
||||
-- | Connect, authenticate, subscribe, then receive and broadcast forever.
|
||||
-- Restarting this action reconnects. All setup sends happen before the
|
||||
@@ -79,12 +80,18 @@ subscribe bus conn ents =
|
||||
|
||||
-- | Undecodable messages are skipped: reconnecting cannot fix a decode
|
||||
-- problem, so crashing here would only produce a hot restart loop.
|
||||
--
|
||||
-- Each read races a one-second timeout: a timeout broadcasts 'Tick' so
|
||||
-- time-based primitives (debounce, rollup, fixed, ...) keep advancing
|
||||
-- even when no state changes arrive.
|
||||
receiveLoop :: Bus -> WS.Connection -> IO Void
|
||||
receiveLoop bus conn = forever $ do
|
||||
msg <- WS.receiveData conn :: IO BL.ByteString
|
||||
case eitherDecode msg of
|
||||
Left err -> putStrLn $ "[reader] skipping undecodable message: " <> err
|
||||
Right v -> atomically $ writeTChan (busInbound bus) v
|
||||
winner <- race (threadDelay 1000000) (WS.receiveData conn)
|
||||
case winner of
|
||||
Left () -> atomically $ writeTChan (busInbound bus) Tick
|
||||
Right msg -> case eitherDecode msg of
|
||||
Left err -> putStrLn $ "[reader] skipping undecodable message: " <> err
|
||||
Right v -> atomically $ writeTChan (busInbound bus) (Event v)
|
||||
|
||||
receiveJSON :: WS.Connection -> IO Value
|
||||
receiveJSON conn = do
|
||||
|
||||
Reference in New Issue
Block a user