packages feed

reactive-banana-automation 0.1.1 → 0.1.2

raw patch · 4 files changed

+213/−14 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Reactive.Banana.Automation: getTimestamp :: Timestamp t => POSIXTime -> t
+ Reactive.Banana.Automation: ClockSignal :: a -> ClockSignal a
+ Reactive.Banana.Automation: clockSignal :: Timestamp t => EventSource (ClockSignal t) -> IO ()
+ Reactive.Banana.Automation: clockSignalAt :: Timestamp t => t -> EventSource (ClockSignal t) -> IO ()
+ Reactive.Banana.Automation: clockSignalBehavior :: Timestamp t => EventSource (ClockSignal t) -> MomentAutomation (Behavior (Maybe (ClockSignal t)))
+ Reactive.Banana.Automation: data ClockSignal a
+ Reactive.Banana.Automation: instance GHC.Base.Functor (Reactive.Banana.Automation.Timestamped t)
+ Reactive.Banana.Automation: instance GHC.Base.Functor Reactive.Banana.Automation.ClockSignal
+ Reactive.Banana.Automation: instance GHC.Base.Functor Reactive.Banana.Automation.Sensed
+ Reactive.Banana.Automation: instance GHC.Classes.Eq a => GHC.Classes.Eq (Reactive.Banana.Automation.ClockSignal a)
+ Reactive.Banana.Automation: instance GHC.Classes.Ord a => GHC.Classes.Ord (Reactive.Banana.Automation.ClockSignal a)
+ Reactive.Banana.Automation: instance GHC.Show.Show a => GHC.Show.Show (Reactive.Banana.Automation.ClockSignal a)
+ Reactive.Banana.Automation: instance Reactive.Banana.Automation.Timestamp Data.Time.LocalTime.Internal.LocalTime.LocalTime
+ Reactive.Banana.Automation: instance Reactive.Banana.Automation.Timestamp Data.Time.LocalTime.Internal.TimeOfDay.TimeOfDay
+ Reactive.Banana.Automation: instance Reactive.Banana.Automation.Timestamp Data.Time.LocalTime.Internal.ZonedTime.ZonedTime
+ Reactive.Banana.Automation.Examples: LCDDisplay :: String -> Actuators
+ Reactive.Banana.Automation.Examples: SprinklerSwitch :: PowerChange -> Actuators
+ Reactive.Banana.Automation.Examples: [clock] :: Sensors -> EventSource (ClockSignal LocalTime)
+ Reactive.Banana.Automation.Examples: [rainGaugeTipSensor] :: Sensors -> EventSource (Sensed ())
+ Reactive.Banana.Automation.Examples: nightLight :: Automation Sensors Actuators
+ Reactive.Banana.Automation.Examples: showBehaviorLCDDisplay :: Show a => (Sensors -> MomentAutomation (Behavior a)) -> Automation Sensors Actuators
+ Reactive.Banana.Automation.Examples: sprinklersStartingAt :: TimeOfDay -> Automation Sensors Actuators
+ Reactive.Banana.Automation.Examples: totalRainfall :: Sensors -> MomentAutomation (Behavior Integer)
+ Reactive.Banana.Automation.Examples: totalRainfallSince :: TimeOfDay -> Sensors -> MomentAutomation (Behavior Integer)
- Reactive.Banana.Automation: sensedAt :: Timestamp t => POSIXTime -> EventSource (Sensed (Timestamped t a)) -> a -> IO ()
+ Reactive.Banana.Automation: sensedAt :: Timestamp t => t -> EventSource (Sensed (Timestamped t a)) -> a -> IO ()
- Reactive.Banana.Automation.Examples: Sensors :: EventSource (Sensed Double) -> EventSource (Sensed (Timestamped POSIXTime Bool)) -> Sensors
+ Reactive.Banana.Automation.Examples: Sensors :: EventSource (Sensed Double) -> EventSource (Sensed (Timestamped POSIXTime Bool)) -> EventSource (ClockSignal LocalTime) -> EventSource (Sensed ()) -> Sensors

Files

CHANGELOG view
@@ -1,3 +1,15 @@+reactive-banana-automation (0.1.2) upstream; urgency=medium++  * Added ClockSignal and related functions.+  * Functor instances for Sensed and Timestamped.+  * Added more Timestamp instances.+  * Generalized sensedAt to work with any Timestamp.+  * Added several more examples, including a night light,+    a behavior for a tipping bucket rain gauge, and sprinkers+    that only run when it's not rained recently.++ -- Joey Hess <id@joeyh.name>  Thu, 03 May 2018 17:28:32 -0400+ reactive-banana-automation (0.1.1) upstream; urgency=medium    * Fix build with ghc 8.2 and reactive-banana 1.2.
Reactive/Banana/Automation.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeSynonymInstances, LambdaCase #-}+{-# LANGUAGE TypeSynonymInstances, LambdaCase, DeriveFunctor #-}  -- | Home (etc) automation using reactive-banana. --@@ -38,6 +38,10 @@ 	sensedNow, 	sensedAt, 	elapsedTimeSince,+	ClockSignal(..),+	clockSignal,+	clockSignalAt,+	clockSignalBehavior, 	-- * Actuators 	PowerChange(..), 	onBehaviorChange,@@ -57,6 +61,7 @@ import Control.Concurrent.STM import Data.Time.Clock import Data.Time.Clock.POSIX+import Data.Time.LocalTime  -- | An Automation receives `Event`s from some sensors and decides what -- to do, controlling the actuators. It is implemented as a reactive-banana@@ -204,7 +209,7 @@ -- Sensors are sometimes not available, or have not provided a value -- yet. data Sensed a = SensorUnavailable | Sensed a-	deriving (Show)+	deriving (Show, Functor)  -- | Create an Event from sensed values. --@@ -218,8 +223,6 @@ 		Sensed a -> Just a  -- | Create a Behavior from sensed values.------ This is essentially just an application of the `stepper` combinator. sensedBehavior :: EventSource (Sensed a) -> MomentAutomation (Behavior (Sensed a)) sensedBehavior s =  	MomentAutomation . stepper SensorUnavailable =<< getEventFrom s@@ -253,19 +256,28 @@ instance (Show t, Show a) => Show (Timestamped t a) where 	show (Timestamped t a) = show t ++ " " ++ show a +instance Functor (Timestamped t) where+	fmap f (Timestamped t a) = Timestamped t (f a)+ -- | Class of values that are timestamps. class Timestamp t where 	getCurrentTimestamp :: IO t-	getTimestamp :: POSIXTime -> t  instance Timestamp POSIXTime where 	getCurrentTimestamp = getPOSIXTime-	getTimestamp = id  instance Timestamp UTCTime where 	getCurrentTimestamp = getCurrentTime-	getTimestamp = posixSecondsToUTCTime +instance Timestamp ZonedTime where+	getCurrentTimestamp = getZonedTime++instance Timestamp LocalTime where+	getCurrentTimestamp = zonedTimeToLocalTime <$> getZonedTime++instance Timestamp TimeOfDay where+	getCurrentTimestamp = localTimeOfDay <$> getCurrentTimestamp+ -- | Call when a sensor has sensed a value, which will be `Timestamped` with -- the current time. sensedNow :: Timestamp t => EventSource (Sensed (Timestamped t a)) -> a -> IO ()@@ -274,8 +286,8 @@ 	gotEvent es (Sensed (Timestamped now a))  -- | Call when a sensor sensed a value with a particular timestamp.-sensedAt :: Timestamp t => POSIXTime -> EventSource (Sensed (Timestamped t a)) -> a -> IO ()-sensedAt ts es a = gotEvent es (Sensed (Timestamped (getTimestamp ts) a))+sensedAt :: Timestamp t => t -> EventSource (Sensed (Timestamped t a)) -> a -> IO ()+sensedAt ts es a = gotEvent es (Sensed (Timestamped ts a))  -- | Given a `Timestamped` `Event` and a function, produces an `Event` -- that contains the elapsed time since the function last matched the@@ -298,6 +310,28 @@ 		| otherwise = Nothing 	reduce (Just (t, v)) = timestamp v - t 	reduce Nothing = 0++-- | A clock signal.+--+-- See `Reactive.Banana.Examples.nightLight` for an example+-- of using clock signals, and how to test code that uses them.+data ClockSignal a = ClockSignal a+	deriving (Show, Eq, Ord, Functor)++-- | Call repeatedly to feed a clock signal to an `Automation`+-- that needs to know what time it is.+clockSignal :: Timestamp t => EventSource (ClockSignal t) -> IO ()+clockSignal es = gotEvent es . ClockSignal =<< getCurrentTimestamp++-- | Call to feed a particular time to an `Automation`.+clockSignalAt :: Timestamp t => t -> EventSource (ClockSignal t) -> IO ()+clockSignalAt t es = gotEvent es (ClockSignal t)++-- | Create a Behavior from a ClockSignal. It will initially be Nothing,+-- and then updates with each incoming clock signal.+clockSignalBehavior :: Timestamp t => EventSource (ClockSignal t) -> MomentAutomation (Behavior (Maybe (ClockSignal t)))+clockSignalBehavior s = MomentAutomation . stepper Nothing +	=<< fmap Just <$> getEventFrom s  -- | For controlling relays and other things that can have -- their power turned on and off.
Reactive/Banana/Automation/Examples.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -fno-warn-unused-imports #-}+ -- | `Automation` examples. View source for the code. -- -- These examples are tested by doctest when building this library.@@ -9,12 +11,16 @@ import Reactive.Banana.Frameworks import Reactive.Banana.Automation import Data.Time.Clock.POSIX+import Data.Time.LocalTime+import Data.Time.Calendar -- imported for doctest examples  -- | We'll use a single Sensors type containing all the sensors -- used by the examples below. data Sensors = Sensors 	{ fridgeTemperature :: EventSource (Sensed Double) 	, motionSensor :: EventSource (Sensed (Timestamped POSIXTime Bool))+	, clock :: EventSource (ClockSignal LocalTime)+	, rainGaugeTipSensor :: EventSource (Sensed ()) 	}  -- | And a single Actuators type containing all the actuators used by the@@ -22,6 +28,8 @@ data Actuators 	= FridgePower PowerChange 	| LightSwitch PowerChange+	| SprinklerSwitch PowerChange+	| LCDDisplay String 	deriving (Show)  -- | For running the examples, you'll need this, to construct a `Sensors`@@ -29,6 +37,8 @@ mkSensors = Sensors 	<$> newAddHandler 	<*> newAddHandler+	<*> newAddHandler+	<*> newAddHandler  -- | A fridge, containing the `fridgeTemperature` sensor and with -- its power controlled by the `FridgePower` actuator.@@ -66,11 +76,11 @@ 	calcpowerchange SensorUnavailable = Nothing 	allowedtemp = Range 1 4 --- | A light that comes on when the `motionSensor` detects movement,--- and remains on for 5 minutes after the last movement.+-- | Turns on a light when the `motionSensor` detects movement,+-- and leaves it on for 5 minutes after the last movement. ----- If this were run in real code, the motion sensor would be triggered--- by running `sensedNow`.+-- If this were run in real code, the motion sensor would trigger+-- calls to `sensedNow`. -- -- But, for testing, it's useful to specify the time that the sensor -- is triggered, using `sensedAt`. Import this module in ghci and run:@@ -100,3 +110,146 @@ 		| t == 0 = Just PowerOn -- motion was just detected 		| t > 300 = Just PowerOff -- 5 minutes since last motion 		| otherwise = Nothing+++-- | Turns on a light at night (after 6 pm), and off during the day (after+-- 6 am).+--+-- If this were run in real code, the clock would be fed+-- by running clockSignal every so often.+--+-- But, for testing, it's useful to specify the time, using +-- `clockSignalAt`. Import this module in ghci and run:+--+-- >>> let day = fromGregorian 2018 1 1+-- >>> runner <- observeAutomation nightLight mkSensors+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day midnight) (clock sensors)+-- [LightSwitch PowerOn]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day midday) (clock sensors)+-- [LightSwitch PowerOff]+nightLight :: Automation Sensors Actuators+nightLight sensors actuators = do+	bclock <- clockSignalBehavior (clock sensors)+	let bhour = (fmap . fmap) (todHour . localTimeOfDay) <$> bclock+	let lightchange = calcchange <$> bhour+	onBehaviorChangeMaybe lightchange (actuators . LightSwitch)+  where+	calcchange (Just (ClockSignal t))+		| t > 18 = Just PowerOn+		| t < 6 = Just PowerOn+		| otherwise = Just PowerOff+	calcchange Nothing = Nothing++-- | Displays a Behavior on the LCD display actuator.+--+-- While it could be used to drive a real LCD, this is mostly useful+-- for testing behaviors.+showBehaviorLCDDisplay :: Show a => (Sensors -> MomentAutomation (Behavior a)) -> Automation Sensors Actuators+showBehaviorLCDDisplay mkb sensors actuators = do+	b <- mkb sensors+	onBehaviorChange b (actuators . LCDDisplay . show)++-- | The rain gauge sensor is a tipping bucket type; the bucket collects 0.01+-- inches of rain and then tips, which triggers the `rainGaugeTipSensor`.+-- This behavior sums up the total rainfall, in hundredths of an inch.+--+-- To test this behavior, we can use `showBehaviorLCDDisplay`:+--+-- >>> runner <- observeAutomation (showBehaviorLCDDisplay totalRainfall) mkSensors+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "1"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "2"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "3"]+totalRainfall :: Sensors -> MomentAutomation (Behavior Integer)+totalRainfall sensors = do+	tipevents <- sensedEvent (rainGaugeTipSensor sensors)+	accumB 0 $ const succ <$> tipevents++-- | This behavior contains the total rainfall since a given `TimeOfDay`.+--+-- To test this behavior, we can use `showBehaviorLCDDisplay`,+-- providing both clock signals and `rainGaugeTipSensor` events:+--+-- >>> let day = fromGregorian 2018 1 1+-- >>> runner <- observeAutomation (showBehaviorLCDDisplay $ totalRainfallSince midnight) mkSensors+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 13 0 0)) (clock sensors)+-- [LCDDisplay "0"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "1"]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 14 0 0)) (clock sensors)+-- [LCDDisplay "1"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "2"]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime (succ day) (TimeOfDay 1 0 0)) (clock sensors)+-- [LCDDisplay "0"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "1"]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime (succ day) (TimeOfDay 2 0 0)) (clock sensors)+-- [LCDDisplay "1"]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [LCDDisplay "2"]+totalRainfallSince :: TimeOfDay -> Sensors -> MomentAutomation (Behavior Integer)+totalRainfallSince tod sensors = do+	clockevents <- getEventFrom (clock sensors)+	bclock <- clockSignalBehavior (clock sensors)+	tipevents <- sensedEvent (rainGaugeTipSensor sensors)+	-- The tip events, with the tip signal replaced with+	-- the clock time when it occurred.+	let tiptimes = bclock <@ tipevents+	-- Combine clock ticks and tip events, with the function+	-- to apply to the running total for each.+	let combined = unionWith (\(f1, t1) (f2, t2) -> (f1 . f2, max t1 t2))+		((\e -> (id, e)) <$> fmap Just clockevents)+		((\e -> (succ, e)) <$> tiptimes)+	fmap fst <$> (accumB (0, Nothing) $ go <$> combined)+  where+	go (f, Just (ClockSignal t)) (n, Just lastzero) =+		let nextzero = succ lastzero+		in if t > LocalTime nextzero tod+			then (0, Just nextzero)+			else (f n, Just lastzero)+	go (f, Just (ClockSignal t)) (n, Nothing) =+		(f n, Just (localDay t))+	go (_, Nothing) v = v++-- | Turns on the sprinklers for an hour each day starting from+-- the specified `TimeOfDay`, but only if the rain gauge collected +-- less than 0.03 inches of rain over the past day.+--+-- >>> let day = fromGregorian 2018 1 1+-- >>> runner <- observeAutomation (sprinklersStartingAt midnight) mkSensors+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 13 0 0)) (clock sensors)+-- [SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 0 1 0)) (clock sensors)+-- [SprinklerSwitch PowerOff,SprinklerSwitch PowerOn]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 0 2 0)) (clock sensors)+-- [SprinklerSwitch PowerOn,SprinklerSwitch PowerOn]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 1 2 0)) (clock sensors)+-- [SprinklerSwitch PowerOn,SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 1 3 0)) (clock sensors)+-- [SprinklerSwitch PowerOff,SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> sensed (rainGaugeTipSensor sensors) ()+-- [SprinklerSwitch PowerOff]+-- >>> runner $ \sensors -> clockSignalAt (LocalTime day (TimeOfDay 0 1 0)) (clock sensors)+-- [SprinklerSwitch PowerOff,SprinklerSwitch PowerOff]+sprinklersStartingAt :: TimeOfDay -> Automation Sensors Actuators+sprinklersStartingAt starttod sensors actuators = do+	brainfall <- totalRainfallSince starttod sensors+	bclock <- clockSignalBehavior (clock sensors)+	let b = calcchange <$> bclock <*> brainfall+	onBehaviorChangeMaybe b (actuators . SprinklerSwitch)+  where+	stoptod = starttod { todHour = (todHour starttod + 1) `mod` 24 }+	calcchange (Just (ClockSignal t)) rain+		| rain >= 3 = Just PowerOff+		| localTimeOfDay t >= starttod && localTimeOfDay t < stoptod = Just PowerOn+		| otherwise = Just PowerOff+	calcchange Nothing _ = Nothing
reactive-banana-automation.cabal view
@@ -1,5 +1,5 @@ Name: reactive-banana-automation-Version: 0.1.1+Version: 0.1.2 Cabal-Version: >= 1.8 License: AGPL-3 Maintainer: Joey Hess <id@joeyh.name>