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
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
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
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
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
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
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