Safe Haskell | None |
---|---|
Language | Haskell98 |
Home (etc) automation using reactive-banana.
Functional Reactive Programming is a natural fit for home automation, which involves sensor values that vary over time and are used to control actuators.
This library provides a framework and some useful types for using the reactive-banana FRP library for home automation.
Its main abstraction is the Automation
which describes how to process
Event
s from Sensor
s and how to drive actuators such as lights,
and relays in response.
See Reactive.Banana.Automation.Examples for several examples of using this library.
- type Automation sensors actuators = sensors -> (actuators -> IO ()) -> MomentAutomation ()
- data MomentAutomation a
- runAutomation :: Automation sensors actuators -> IO sensors -> (actuators -> IO ()) -> (sensors -> IO ()) -> IO ()
- observeAutomation :: Automation sensors actuators -> IO sensors -> IO ((sensors -> IO ()) -> IO [actuators])
- type EventSource a = (AddHandler a, a -> IO ())
- gotEvent :: EventSource a -> a -> IO ()
- getEventFrom :: EventSource a -> MomentAutomation (Event a)
- onEvent :: Event a -> (a -> IO ()) -> MomentAutomation ()
- data Sensed a
- = SensorUnavailable
- | Sensed a
- sensedEvent :: EventSource (Sensed a) -> MomentAutomation (Event a)
- sensedBehavior :: EventSource (Sensed a) -> MomentAutomation (Behavior (Sensed a))
- sensed :: EventSource (Sensed a) -> a -> IO ()
- (=:) :: EventSource (Sensed a) -> a -> IO ()
- data Timestamped t a = Timestamped {}
- class Timestamp t where
- sensedNow :: Timestamp t => EventSource (Sensed (Timestamped t a)) -> a -> IO ()
- sensedAt :: Timestamp t => POSIXTime -> EventSource (Sensed (Timestamped t a)) -> a -> IO ()
- elapsedTimeSince :: (Num t, Timestamp t) => (a -> Bool) -> Event (Timestamped t a) -> MomentAutomation (Event t)
- data PowerChange
- onBehaviorChange :: Behavior a -> (a -> IO ()) -> MomentAutomation ()
- onBehaviorChangeMaybe :: Behavior (Maybe a) -> (a -> IO ()) -> MomentAutomation ()
- data Range t = Range t t
- belowRange :: Ord t => t -> Range t -> Bool
- aboveRange :: Ord t => t -> Range t -> Bool
- inRange :: Ord t => t -> Range t -> Bool
- extendRange :: Ord t => Range t -> t -> Range t
Framework
type Automation sensors actuators = sensors -> (actuators -> IO ()) -> MomentAutomation () Source #
An Automation receives Event
s from some sensors and decides what
to do, controlling the actuators. It is implemented as a reactive-banana
event network description.
For example, let's make an automation for a fridge, which has a temperature sensor and a relay controlling its power, and should run as needed to keep the temperature in a safe range, while minimizing compressor starts.
data Sensors = Sensors { fridgeTemperature :: EventSource (Sensed Double) } data Actuators = FridgePower PowerChange deriving (Show) fridge :: Automation Sensors Actuators fridge sensors actuators = do btemperature <- sensedBehavior (fridgeTemperature sensors) let bpowerchange = calcpowerchange <$> btemperature onBehaviorChange bpowerchange (actuators . FridgePower) where calcpowerchange (Sensed temp) | temp `belowRange` allowedtemp = Just PowerOff | temp `aboveRange` allowedtemp = Just PowerOn | otherwise = Nothing calcpowerchange SensorUnavailable = Nothing allowedtemp = Range 1 4
data MomentAutomation a Source #
This is simply a wrapper around reactive-banana's MomentIO
,
but without the MonadIO
instance, so an Automation
using this monad
is limited to using its sensors and actuators for IO. That allows
it to be fully tested using observeAutomation
.
All of Reactive.Banana.Combinators can be used with this monad.
runAutomation :: Automation sensors actuators -> IO sensors -> (actuators -> IO ()) -> (sensors -> IO ()) -> IO () Source #
Runs an Automation, given a constructor for the sensors, an IO action to drive the actuators, and an IO action that feeds data into the sensors.
Continuing the above example of a fridge, here's how to run it:
mkSensors :: IO Sensors mkSensors = Sensors <$> newAddHandler driveActuators :: Actuators -> IO () driveActuators = print getFridgeTemperature :: IO Double getFridgeTemperature = ... main = runAutomation fridge mkSensors driveActuators $ \sensors -> do getFridgeTemperature >>= sensed (fridgeTemperature sensors)
Note that this function does not return; the sensor feeding action is run in a loop.
observeAutomation :: Automation sensors actuators -> IO sensors -> IO ((sensors -> IO ()) -> IO [actuators]) Source #
Allows observing what an Automation does. Designed to be especially useful for testing.
The Automation is started, and a runner action is returned. The runner allows updating the sensors, and returns what the Automation wants to do in response.
For example, in ghci:
> runner <- observeAutomation fridge mkSensors > runner $ \sensors -> fridgeTemperature sensors =: 6 [FridgeRelay PowerOn] > runner $ \sensors -> fridgeTemperature sensors =: 3 [] > runner $ \sensors -> fridgeTemperature sensors =: 0.5 [FridgeRelay PowerOff]
Note that internal state is maintained between calls to the runner.
Events
type EventSource a = (AddHandler a, a -> IO ()) Source #
A source of events.
gotEvent :: EventSource a -> a -> IO () Source #
Call this to trigger an event.
getEventFrom :: EventSource a -> MomentAutomation (Event a) Source #
Get an Event from an EventSource.
onEvent :: Event a -> (a -> IO ()) -> MomentAutomation () Source #
Runs an action when an event occurs.
Sensors
A value read from a sensor.
Sensors are sometimes not available, or have not provided a value yet.
sensedEvent :: EventSource (Sensed a) -> MomentAutomation (Event a) Source #
Create an Event from sensed values.
The Event only contains values when the sensor provided a reading, not times when it was unavailable.
sensedBehavior :: EventSource (Sensed a) -> MomentAutomation (Behavior (Sensed a)) Source #
Create a Behavior from sensed values.
This is essentially just an application of the stepper
combinator.
sensed :: EventSource (Sensed a) -> a -> IO () Source #
Call when a sensor has sensed a value.
getFridgeTemperature >>= sensed (fridgeTemperature sensors)
Time
data Timestamped t a Source #
A timestamped value.
In reactive-banana, an Event
is tagged with its time of occurrence,
but that internal representation of time is never exposed. It can be
useful to have an Event
timestamped as occurring at a specific wall
clock time.
See motionActivatedLight
for an example
of using timestamped values, and how to test code that uses them.
class Timestamp t where Source #
Class of values that are timestamps.
getCurrentTimestamp :: IO t Source #
getTimestamp :: POSIXTime -> t Source #
sensedNow :: Timestamp t => EventSource (Sensed (Timestamped t a)) -> a -> IO () Source #
Call when a sensor has sensed a value, which will be Timestamped
with
the current time.
sensedAt :: Timestamp t => POSIXTime -> EventSource (Sensed (Timestamped t a)) -> a -> IO () Source #
Call when a sensor sensed a value with a particular timestamp.
elapsedTimeSince :: (Num t, Timestamp t) => (a -> Bool) -> Event (Timestamped t a) -> MomentAutomation (Event t) Source #
Given a Timestamped
Event
and a function, produces an Event
that contains the elapsed time since the function last matched the
event's value.
motionActivatedLight
has a good example
of using this.
Actuators
data PowerChange Source #
For controlling relays and other things that can have their power turned on and off.
onBehaviorChange :: Behavior a -> (a -> IO ()) -> MomentAutomation () Source #
Runs an action when a behavior's value changes.
onBehaviorChangeMaybe :: Behavior (Maybe a) -> (a -> IO ()) -> MomentAutomation () Source #
Variant of onBehaviorChange
that does nothing when a behavior
changes to Nothing.
Ranges
The range between two values (inclusive).
Note that the position of the two values in the Range constructor is not significant; Range 1 10 == Range 10 1
Range t t |