Sparrow by Example

We will use seven smart home automations to illustrate how easily Sparrow’ patterns can encode complex interactions and coordinations between the actors.

Smart Home Scenarios

E1. Turn on the lights of the bathroom if someone enters in it, and its ambient light is less than 40 lux.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
defmodule Automation1 do
  use Sparrow.Actor

  pattern bathroom_occupied as {:motion, idm, :on, :bathroom} 
                           and {:ambient_light, idal, value, :bathroom} 
                           and {:light, idl, :off, :bathroom} 
                           when value > 40,
                           options: [last: true]    

  reaction turn_on_light(l, i, t), do: # Smart home API call 

  react_to bathroom_occupied, with: turn_on_light

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
defmodule Automation1 do

  def start do
    spawn(Automation1, :loop, [{100, :off}])
  end 

  def loop({ambient_light, light_status}) do                      

    state =                                                       
      receive do                                                  
        {:motion, _id, :on, :bathroom} ->                         

          case ambient_light <= 40 and light_status == :off do    
            true ->
              turn_on_light()
              {ambient_light, :on}                             

            false -> {ambient_light, :off}                      
          end

        {:ambient_light, _id, value, :bathroom} ->                
          {value, light_status}                                   

        {:light, _id, value, :bathroom} ->                        
          ambient_light, value}                                   
      end                                                         

    loop(state)                                                  
  end

  def turn_on_light(), do: # Smart home API call 

end
E2. Turn off the lights in a room after two minutes without detecting any movement.
1
2
3
4
5
6
7
8
defmodule Automation2 do
  use Sparrow.Actor

  pattern turn_off as not {:motion, id, :on, :bathroom}[window: {2, :mins}] 
                      and {:light, idl, :on, :bathroom},
                      options: [last: true]  

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
defmodule Automation2 do

  def start do
    spawn(Automation2, :loop, [{nil, :off}])
  end

  def loop({timer_ref, light_status}) do                          

    state =                                                       
      receive do                                                  
        {:motion, _id, :on, :bathroom} ->                         
         
          timer_ref =                                             
            case light_status == :on do                           
             true -> set_timer(timer_ref)
             false -> timer_ref                                   
            end
          {timer_ref, light_status}                               

        {:light, _id, value, :bathroom} ->  
          timer_ref =                                             
            case value == :on do                                  
              true -> set_timer(timer_ref)
              false -> timer_ref                                  
            end
          {timer_ref, value}                                      

        :no_motion ->
          turn_off_ligth()
          {nil, :off}                                             
      end

    loop(state)                                                   
  end

  
  def set_timer(nil), do: Process.send_after(self(), :no_motion, 5000)    
  def set_timer(timer_ref) do                                                                 
    Process.cancel_timer(timer_ref)                                                           
    Process.send_after(self(), :no_motion, 5000) # two min = 120000                                             
  end  

  def turn_off_ligth() , do: # Smart home API call 

end
E3. Send a notification when a window has been open for over an hour.
1
2
3
4
5
6
7
defmodule Automation3 do
  use Sparrow.Actor

  pattern send_alert as not {:contact, :open, room} [window: {60, :secs}], 
                        options: [last: true]

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
defmodule Automation3 do

  def start do
    spawn(Automation3, :loop, [{nil, :open}])
  end

  def loop({timer_ref, window_status}) do                             

    state =                                                           
      receive do                                                      
        {:contact, _id, :open, :bathroom} ->  
          timer_ref = Process.send_after(self(), :time_alert, 5000)   
          {timer_ref, :open}                                          

        {:contact, _id, :closed, :bathroom} ->  
          Process.cancel_timer(timer_ref)                             
          {nil, :close}                                               

        :time_alert -> 
          send_alert()          
          {nil, window_status}                                        
      end                                                             

    loop(state)                                                       
  end

  def send_alert(), do: # send alert to to the smart home's users

end
E4. Send a notification if someone presses the doorbell, but only if no notification was sent in the past 30 seconds.
1
2
3
4
5
6
defmodule Automation4 do
  use Sparrow.Actor

  pattern door_bell as {:door_bell, id}[debounce: {30, :secs}] 

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defmodule Automation4 do
  require Timex

  def start do
    last_ring = Timex.shift(Timex.now(), hours: -24)
    spawn(Automation4, :loop, [last_ring])
  end

  def loop(last_ring) do                                                              

    state =                                                                           
      receive do                                                                      
        {:doorbell, _id, :pressed, :front_door} ->   
          case Timex.before?(Timex.shift(last_ring, seconds: 30), Timex.now()) do     
              true ->
                IO.puts "Ring"
                Timex.now()                                                           
              false -> last_ring                                                      
          end
      end

    loop(state)                                                                       
  end

  def notify(), do: # Send doorbell notification

end
E5. Detect home arrival or leaving based on a particular sequence of messages, and activate the corresponding scene.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
defmodule Automation5 do
  use Sparrow.Actor

  pattern motion as {:motion, id, :on, location}                                
  pattern m_front_door as motion{location= :front_door}                         
  pattern m_entrance_hall as motion{location= :entrance_hall, id~> mid}         
  pattern c_front_door as {:contact, cid, :open, :front_door}                   

  pattern occupied_home as m_front_door and c_front_door and m_entrance_hall,   
                                            options: [ interval: {60, :secs},   
                                                       seq: true,               
                                                       last: true ]

  pattern empty_home as m_entrance_hall and c_front_door and m_front_door,      
                                      options: [ interval: {60, :secs},         
                                                 seq: true,                     
                                                 last: true ]

  reaction activate_home_scene(l, i, t), do:  # API call to activate the occupied home scene
  reaction activate_empty_scene(l, i, t), do: # API call to activate the empty home scene

  react_to occupied_home, with: activate_home_scene
  react_to empty_home, with: activate_leave_scene

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
defmodule Automation5 do
  require Timex

  def start do
    old_time = Timex.shift(Timex.now(), hours: -24)
    spawn(Automation5, :loop, [{old_time, old_time, old_time}])
  end

  def loop({m_door, m_hall, c_door}) do                                               

    state =                                                                           
      receive do                                                                      
        {:motion, _id, :on, :front_door, m_door_dt} ->   
          if Timex.before?(Timex.shift(m_door_dt, seconds: -60), m_hall) do           
            if Timex.after?(m_door_dt, c_door) and Timex.after?(c_door, m_hall) do    
              activate_empty_scene()
            end
          end

          {m_door_dt, m_hall, c_door}                                                 

        {:motion, _id, :on, :entrance_hall, m_hall_dt} ->   
          if Timex.before?(Timex.shift(m_hall_dt, seconds: -60), m_door) do          
            if Timex.after?(m_hall_dt, c_door) and Timex.after?(c_door, m_door) do    
              activate_home_scene()
            end
          end

          {m_door, m_hall_dt, c_door}                                                 

        {:contact, _id, :open, :front_door, dt} ->   
          {m_door, m_hall, dt}                                                       
      end

    loop(state)                                                                       
  end
  def activate_home_scene(), do:  # API call to activate the occupied home scene
  def activate_empty_scene(), do: # API call to activate the empty home scene
end
E6. Send a notification if the combined electricity consumption of the past three weeks is greater than 200 kWh.
1
2
3
4
5
6
7
8
9
defmodule Automation6 do
  use Sparrow.Actor

  pattern electicity_alert as {:consumption, meter_id, @value}[window: {3, :weeks}] 
                           |> fold(0, fn({_,_,v}, acc)-> acc+v end)                 
                           |> bind(total)                                           
                           |> total > 200                                           

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
defmodule Automation6 do
  require Timex

  def start do
    spawn(Automation6, :loop, [{0, []}])
  end

  def loop({counter, values}) do                                        

    state =                                                              
      receive do                                                         
        {:daily_consumption, _id, value} when counter <=4 -> # 20 reads  
          { counter+1, values ++ [value]}                                

        {:daily_consumption, _id, value} when counter < 5 -> # 21 reads  
          values = values ++ [value]                                     
          check_consumption(values)
          {counter+1, values}                                             

        {:daily_consumption, _id, value} ->                               
          [_first | rest] = values                                        
          values = rest ++ [value]                                        
          check_consumption(values)
          {counter, values}                                               
      end

    loop(state)                                                          
  end

  def check_consumption(values) do                                        
    consumption = Enum.reduce(values, 0, fn i, acc -> i+acc end)          
    if consumption > 200 do                                               
      IO.puts "send notification"
    end
  end

end
E7. Send a notification if the boiler fires three Floor Heating Failures and one Internal Failure within the past hour, but only if no notification was sent in the past hour.
1
2
3
4
5
6
7
defmodule Automation7 do
  use Sparrow.Actor

  pattern heating_failure as {:heating_f, id, :fhs_failure}[every: 3] and {:heating_f, id, :is_failure}, 
                          options: [debounce: {60, :mins}]                                               

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
defmodule Automation7 do
  require Timex
  @codes [:fhs_failure, :is_failure]

  def start do
    old_time = Timex.shift(Timex.now(), hours: -24)
    spawn(Automation7, :loop, [{ %{:fhs_failure => 0, :is_failure => 0}, old_time}])
  end


  def loop({counter, last_notif}) do                                      

    state =                                                              
      receive do                                                          
        {:boiler, _id, code} when code in @codes ->                       
          {_, counter} = Map.get_and_update(counter, code, fn val -> {val, val+1} end)      
          Process.send_after(self(), {:timer, code}, 5000) # 1 hour = 3.600.000 ms          
          last_notif = check_constraints(counter, last_notif)                               
          {counter, last_notif}                                                             
        {:timer, code} ->                                                                   
          {_, counter} = Map.get_and_update(counter, code, fn val -> {val,  val-1 } end)    
          {counter, last_notif}                                                             
      end

    loop(state)                                                                             
  end

  defp check_constraints(counter, last_notif) do                                      
    case counter.fhs_failure >= 3 and counter.is_failure >= 1 do                      
      true ->
        case Timex.before?(last_notif, Timex.shift(Timex.now(), seconds: -10)) do  #60  
          true ->
            notify()
            Timex.now()                                                              
          false -> last_notif                                                         
        end
      false -> last_notif                                                             
    end                                                                               
  end

  def notify(), do: # send notification

end