ruby-macrodroid 0.8.11 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,10 @@
3
3
 
4
4
  # This file contains the following classes:
5
5
  #
6
+ # ## Nlp classes
7
+ #
8
+ # TriggersNlp ActionsNlp ConstraintsNlp
9
+ #
6
10
  # ## Macro class
7
11
  #
8
12
  # Macro
@@ -12,11 +16,372 @@
12
16
  VAR_TYPES = {
13
17
  String: [2, :string_value],
14
18
  TrueClass: [0, :boolean_value],
15
- TrueClass: [0, :boolean_value],
19
+ FalseClass: [0, :boolean_value],
16
20
  Integer: [1, :int_value],
17
21
  Float: [3, :decimal_value]
18
22
  }
19
23
 
24
+ class TriggersNlp
25
+ include AppRoutes
26
+
27
+ def initialize(macro=nil)
28
+
29
+ super()
30
+ params = {macro: macro}
31
+ triggers(params)
32
+
33
+ end
34
+
35
+ def triggers(params)
36
+
37
+ # e.g. at 7:30pm daily
38
+ get /^(?:at )?(\d+:\d+(?:[ap]m)?) daily/i do |time, days|
39
+ [TimerTrigger, {time: time,
40
+ days: %w(Mon Tue Wed Thu Fri Sat Sun).join(', ')}]
41
+ end
42
+
43
+ get /^(?:at )?(\d+:\d+(?:[ap]m)?) (?:on )?(.*)/i do |time, days|
44
+ [TimerTrigger, {time: time, days: days}]
45
+ end
46
+
47
+ # time.is? 'at 18:30pm on Mon or Tue'
48
+ get /^time.is\? ['"](?:at )?(\d+:\d+(?:[ap]m)?) (?:on )?(.*)['"]/i do |time, days|
49
+ [TimerTrigger, {time: time, days: days.gsub(' or ',', ')}]
50
+ end
51
+
52
+ get /^shake[ _]device\??$/i do
53
+ [ShakeDeviceTrigger, {}]
54
+ end
55
+
56
+ get /^Flip Device (.*)$/i do |motion|
57
+ facedown = motion =~ /Face Up (?:->|to) Face Down/i
58
+ [FlipDeviceTrigger, {face_down: facedown }]
59
+ end
60
+
61
+ get /^flip_device_down\?$/i do
62
+ [FlipDeviceTrigger, {face_down: true }]
63
+ end
64
+
65
+ get /^flip_device_up\?$/i do
66
+ [FlipDeviceTrigger, {face_down: false }]
67
+ end
68
+
69
+ get /^Failed Login Attempt$/i do
70
+ [FailedLoginTrigger, {}]
71
+ end
72
+
73
+ get /^failed_login?$/i do
74
+ [FailedLoginTrigger, {}]
75
+ end
76
+
77
+ get /^Geofence (Entry|Exit) \(([^\)]+)/i do |direction, name|
78
+ enter_area = direction.downcase.to_sym == :entry
79
+ [GeofenceTrigger, {name: name, enter_area: enter_area}]
80
+ end
81
+
82
+ get /^location (entered|exited) \(([^\)]+)/i do |direction, name|
83
+ enter_area = direction.downcase.to_sym == :entered
84
+ [GeofenceTrigger, {name: name, enter_area: enter_area}]
85
+ end
86
+
87
+ # eg. Proximity Sensor (Near)
88
+ #
89
+ get /^Proximity Sensor \(([^\)]+)\)/i do |distance|
90
+
91
+ [ProximityTrigger, {distance: distance}]
92
+ end
93
+
94
+ # eg. Proximity near
95
+ #
96
+ get /^Proximity (near|far|slow wave|fast wave)/i do |distance|
97
+
98
+ [ProximityTrigger, {distance: distance}]
99
+ end
100
+
101
+ get /^WebHook \(Url\)/i do
102
+ [WebHookTrigger, params]
103
+ end
104
+
105
+ get /^WebHook/i do
106
+ [WebHookTrigger, params]
107
+ end
108
+
109
+ get /^wh/i do
110
+ [WebHookTrigger, params]
111
+ end
112
+
113
+ # MacroDroid specific ---------------------------------------------------------------
114
+
115
+ get /^EmptyTrigger$/i do
116
+ [EmptyTrigger, params]
117
+ end
118
+
119
+ end
120
+
121
+ alias find_trigger run_route
122
+
123
+ def to_s(colour: false)
124
+ 'TriggersNlp ' + @h.inspect
125
+ end
126
+
127
+ alias to_summary to_s
128
+ end
129
+
130
+ class ActionsNlp
131
+ include AppRoutes
132
+
133
+ def initialize(macro=nil)
134
+
135
+ super()
136
+ params = {macro: macro}
137
+ actions(params)
138
+
139
+ end
140
+
141
+ def actions(params)
142
+
143
+ # e.g. message popup: hello world!
144
+ get /^message popup: (.*)/i do |msg|
145
+ [ToastAction, {msg: msg}]
146
+ end
147
+
148
+ # e.g. Popup Message 'hello world!'
149
+ get /^Popup[ _]Message ['"]([^'"]+)/i do |msg|
150
+ [ToastAction, {msg: msg}]
151
+ end
152
+
153
+ # e.g. Popup Message\n hello world!
154
+ get /^Popup Message\n\s+(.*)/im do |msg|
155
+ [ToastAction, {msg: msg}]
156
+ end
157
+
158
+ # e.g. Popup Message
159
+ get /^Popup Message$/i do
160
+ [ToastAction, {}]
161
+ end
162
+
163
+ # e.g. say current time
164
+ get /^say current[ _]time/i do
165
+ [SayTimeAction, {}]
166
+ end
167
+
168
+ get /^Torch :?(.*)/i do |onoffstate|
169
+ state = %w(on off toggle).index onoffstate.downcase
170
+ [CameraFlashLightAction, {state: state}]
171
+ end
172
+
173
+ get /^Take Picture/i do
174
+ [TakePictureAction, {}]
175
+ end
176
+
177
+ get /^take_picture/i do
178
+ [TakePictureAction, {}]
179
+ end
180
+
181
+ # -- DEVICE ACTIONS ------------------------------------------------------
182
+
183
+ get /^Speak text \(([^\)]+)\)/i do |text|
184
+ [SpeakTextAction, {text: text}]
185
+ end
186
+
187
+ get /^Speak text ['"]([^'"]+)/i do |text|
188
+ [SpeakTextAction, {text: text}]
189
+ end
190
+
191
+ get /^Speak text$/i do |text|
192
+ [SpeakTextAction, {}]
193
+ end
194
+
195
+ get /^Vibrate \(([^\)]+)/i do |pattern|
196
+ [VibrateAction, {pattern: pattern}]
197
+ end
198
+
199
+ # e.g. Display Notification: Hi there: This is the body of the message
200
+ get /^Display Notification: ([^:]+): [^$]+$/i do |subject, text|
201
+ [NotificationAction, {subject: subject, text: text}]
202
+ end
203
+
204
+
205
+ # e.g. Enable Wifi
206
+ get /^(Enable|Disable) Wifi$/i do |raw_state|
207
+
208
+ state = raw_state.downcase.to_sym == :enable ? 0 : 1
209
+ [SetWifiAction, {state: state}]
210
+
211
+ end
212
+
213
+ # e.g. Play: Altair
214
+ get /^Play: (.*)$/i do |name|
215
+
216
+ [PlaySoundAction, {file_path: name}]
217
+
218
+ end
219
+
220
+ # e.g. Launch Settings
221
+ get /^Launch (.*)$/i do |application|
222
+
223
+ h = {
224
+ application_name: application,
225
+ package_to_launch: 'com.android.' + application.downcase
226
+ }
227
+ [LaunchActivityAction, h]
228
+
229
+ end
230
+
231
+ # e.g. HTTP GET http://someurl.com/something
232
+ get /^HTTP GET ([^$]+)$/i do |url|
233
+
234
+ [OpenWebPageAction, url_to_open: url]
235
+
236
+ end
237
+
238
+ get /^HTTP GET$/i do
239
+
240
+ [OpenWebPageAction, {}]
241
+
242
+ end
243
+
244
+ # e.g. webhook entered_kitchen
245
+ #
246
+ get /(?:webhook|HTTP GET) ([^$]+)$/i do |s|
247
+ key = s =~ /^http/ ? :url_to_open : :identifier
248
+ [OpenWebPageAction, {key => s}]
249
+ end
250
+
251
+ #
252
+ get /^WebHook \(Url\)/i do
253
+ [OpenWebPageAction, params]
254
+ end
255
+
256
+ # e.g. webhook entered_kitchen
257
+ #
258
+ get /^webhook$/i do
259
+ [OpenWebPageAction, params]
260
+ end
261
+
262
+ # -- Location ---------------------------------------------------------
263
+
264
+ get /^Force Location Update$/i do
265
+ [ForceLocationUpdateAction, params]
266
+ end
267
+
268
+ get /^Share Location$/i do
269
+ [ShareLocationAction, params]
270
+ end
271
+
272
+ #a: Keep Device Awake Screen On Until Disabled
273
+ #
274
+ get /Keep Device Awake Screen On Until Disabled/i do
275
+ [KeepAwakeAction, {enabled: true, permanent: true, screen_option: 0}]
276
+ end
277
+
278
+
279
+ #a: Keep Device Awake Screen On 1h 1m 1s
280
+ #
281
+ get /Keep Device Awake Screen On ([^$]+)/i do |duration|
282
+
283
+ a = duration.split.map(&:to_i)
284
+ secs = Subunit.new(units={minutes:60, hours:60, seconds: 60}, a).to_i
285
+
286
+ h = {
287
+ permanent: true, screen_option: 0, seconds_to_stay_awake_for: secs
288
+ }
289
+ [KeepAwakeAction, h]
290
+ end
291
+
292
+ get /Keep Device Awake$/i do
293
+ [KeepAwakeAction, params]
294
+ end
295
+
296
+ #a: Disable Keep Awake
297
+ #
298
+ get /Disable Keep Awake/i do
299
+ [KeepAwakeAction, {enabled: false, screen_option: 0}]
300
+ end
301
+
302
+ #e.g a: if Airplane mode enabled
303
+ #
304
+ get /if (.*)/i do
305
+ [IfConditionAction, {}]
306
+ end
307
+
308
+ get /End If/i do
309
+ [EndIfAction, {}]
310
+ end
311
+
312
+ # -- MacroDroid Specific ------------------------------------------------
313
+ #
314
+ get /^Set Variable$/i do
315
+ [SetVariableAction, {}]
316
+ end
317
+
318
+ end
319
+
320
+ alias find_action run_route
321
+
322
+
323
+ end
324
+
325
+ class ConstraintsNlp
326
+ include AppRoutes
327
+
328
+ def initialize()
329
+
330
+ super()
331
+ params = {}
332
+ constraints(params)
333
+
334
+ end
335
+
336
+ def constraints(params)
337
+
338
+ # Device State
339
+
340
+ get /^Device (locked|unlocked)/i do |state|
341
+ [DeviceLockedConstraint, {locked: state.downcase == 'locked'}]
342
+ end
343
+
344
+ get /^airplane mode (.*)/i do |state|
345
+ [AirplaneModeConstraint, {enabled: (state =~ /^enabled|on$/i) == 0}]
346
+ end
347
+
348
+ #
349
+
350
+ # -- Sensors -----------------------------------
351
+ #
352
+ get /^Light Sensor (Less|Greater) than (50.0)lx/i do |operator, val|
353
+
354
+ level, option = operator.downcase == 'less' ? [-1,0] : [1,1]
355
+
356
+ h = {
357
+ light_level: level,
358
+ light_level_float: val,
359
+ option: option
360
+ }
361
+
362
+ [LightLevelConstraint, h]
363
+ end
364
+
365
+ get /^Proximity Sensor: (Near|Far)/i do |distance|
366
+ [ProximitySensorConstraint, {near: distance.downcase == 'near'}]
367
+ end
368
+
369
+
370
+ # -- Screen and Speaker ---------------------------
371
+ #
372
+ get /^Screen (On|Off)/i do |state|
373
+ [ScreenOnOffConstraint, {screen_on: state.downcase == 'on'}]
374
+ end
375
+
376
+ end
377
+
378
+ alias find_constraint run_route
379
+
380
+ end
381
+
382
+
383
+ class MacroError < Exception
384
+ end
20
385
 
21
386
  class Macro
22
387
  using ColouredText
@@ -24,15 +389,17 @@ class Macro
24
389
 
25
390
  attr_reader :local_variables, :triggers, :actions, :constraints,
26
391
  :guid, :deviceid
27
- attr_accessor :title, :description
392
+ attr_accessor :title, :description, :remote_url
28
393
 
29
- def initialize(name=nil, geofences: nil, deviceid: nil, debug: false)
394
+ def initialize(name=nil, geofences: nil, deviceid: nil, remote_url: nil,
395
+ debug: false)
30
396
 
31
397
  @title, @geofences, @deviceid, @debug = name, geofences, deviceid, debug
398
+ @remote_url = remote_url
32
399
 
33
400
  puts 'inside Macro#initialize' if @debug
34
401
 
35
- @local_variables, @triggers, @actions, @constraints = [], [], [], []
402
+ @local_variables, @triggers, @actions, @constraints = {}, [], [], []
36
403
  @h = {}
37
404
 
38
405
  end
@@ -59,9 +426,13 @@ class Macro
59
426
  end
60
427
 
61
428
  def to_h()
429
+
430
+ a = @local_variables.map do |k,v|
431
+ varify(k,v).to_camelcase.map{|key,value| ['m_' + key, value]}.to_h
432
+ end
62
433
 
63
434
  h = {
64
- local_variables: varify(@local_variables),
435
+ local_variables: a,
65
436
  m_trigger_list: @triggers.map(&:to_h),
66
437
  m_action_list: @actions.map(&:to_h),
67
438
  m_category: @category,
@@ -71,7 +442,7 @@ class Macro
71
442
  m_excludeLog: false,
72
443
  m_GUID: guid(),
73
444
  m_isOrCondition: false,
74
- m_enabled: false,
445
+ m_enabled: true,
75
446
  m_descriptionOpen: false,
76
447
  m_headingColor: 0
77
448
  }
@@ -96,7 +467,7 @@ class Macro
96
467
  if h[:local_variables].any? and h[:local_variables].first.any? then
97
468
 
98
469
  @local_variables = h[:local_variables].map do |var|
99
-
470
+
100
471
  val = case var[:type]
101
472
  when 0 # boolean
102
473
  var[:boolean_value]
@@ -115,7 +486,7 @@ class Macro
115
486
 
116
487
  # fetch the triggers
117
488
  @triggers = h[:trigger_list].map do |trigger|
118
- puts 'trigger: ' + trigger.inspect
489
+ puts 'trigger: ' + trigger.inspect if @debug
119
490
  #exit
120
491
  object(trigger.to_snake_case)
121
492
 
@@ -194,31 +565,103 @@ class Macro
194
565
 
195
566
  @title = node.text('macro') || node.attributes[:name]
196
567
 
197
- @local_variables = node.xpath('variable').map do |e|
198
- e.text.split(/: +/,2)
568
+ d = node.element('description')
569
+
570
+ if d then
571
+
572
+ desc = []
573
+ desc << d.text.strip
574
+
575
+ if d.element('item/description') then
576
+ desc << d.text('item/description').strip
577
+ end
578
+
579
+ @description = desc.join("\n")
580
+
199
581
  end
200
582
 
583
+ node.xpath('variable').each {|e| set_var(*e.text.to_s.split(/: */,2)) }
584
+
201
585
  #@description = node.attributes[:description]
202
586
 
203
587
  tp = TriggersNlp.new(self)
204
588
 
205
- @triggers = node.xpath('trigger').map do |e|
589
+ @triggers = node.xpath('trigger').flat_map do |e|
206
590
 
207
591
  r = tp.find_trigger e.text
208
592
 
209
593
  puts 'found trigger ' + r.inspect if @debug
210
594
 
211
- if r then
212
- if r[0] == GeofenceTrigger then
213
- GeofenceTrigger.new(r[1], geofences: @geofences)
595
+ item = e.element('item')
596
+ if item then
597
+
598
+ if item.element('description') then
599
+
600
+ item.xpath('description').map do |description|
601
+
602
+ inner_lines = description.text.to_s.strip.lines
603
+ puts 'inner_lines: ' + inner_lines.inspect if @debug
604
+
605
+ trigger = if e.text.to_s.strip.empty? then
606
+ inner_lines.shift.strip
607
+ else
608
+ e.text.strip
609
+ end
610
+
611
+ puts 'trigger: ' + trigger.inspect if @debug
612
+
613
+ r = tp.find_trigger trigger
614
+ puts 'r: ' + r.inspect if @debug
615
+ #o = r[0].new([description, self]) if r
616
+ o = object_create(r[0], [description, self]) if r
617
+ puts 'after o' if @debug
618
+ o
619
+
620
+ end
621
+
622
+ else
623
+
624
+ trigger = e.text.strip
625
+ r = tp.find_trigger trigger
626
+
627
+ a = e.xpath('item/*')
628
+
629
+ h = if a.any? then
630
+ a.map {|node| [node.name.to_sym, node.text.to_s]}.to_h
631
+ else
632
+ {}
633
+ end
634
+
635
+ r = tp.find_trigger trigger
636
+ #r[0].new(h) if r
637
+ if r then
638
+ object_create(r[0], h)
639
+ else
640
+ raise MacroError, 'App-routes: Trigger "' + trigger + '" not found'
641
+ end
642
+
643
+ end
644
+
645
+ else
646
+
647
+ trigger = e.text.strip
648
+ r = tp.find_trigger trigger
649
+ #r[0].new(r[1]) if r
650
+
651
+ if r then
652
+ object_create(r[0],r[1])
214
653
  else
215
- r[0].new(r[1])
654
+ raise MacroError, 'App-routes: Trigger "' + trigger + '" not found'
216
655
  end
656
+
217
657
  end
218
658
 
659
+
219
660
  end
661
+
662
+
220
663
 
221
- ap = ActionsNlp.new
664
+ ap = ActionsNlp.new self
222
665
 
223
666
  @actions = node.xpath('action').flat_map do |e|
224
667
 
@@ -245,8 +688,10 @@ class Macro
245
688
 
246
689
  r = ap.find_action action
247
690
  puts 'r: ' + r.inspect if @debug
248
- o = r[0].new(description) if r
249
- puts 'after o'
691
+ puts 'description: ' + description.xml.inspect if @debug
692
+ #o = r[0].new([description, self]) if r
693
+ o = object_create(r[0],[description, self]) if r
694
+ puts 'after o' if @debug
250
695
  o
251
696
 
252
697
  end
@@ -254,6 +699,7 @@ class Macro
254
699
  else
255
700
 
256
701
  action = e.text.strip
702
+ puts 'action: ' + action.inspect if @debug
257
703
  r = ap.find_action action
258
704
 
259
705
  a = e.xpath('item/*')
@@ -263,9 +709,11 @@ class Macro
263
709
  else
264
710
  {}
265
711
  end
712
+ puts 'h: ' + h.inspect if @debug
266
713
 
267
- r = ap.find_action action
268
- r[0].new(h) if r
714
+ #r = ap.find_action action
715
+ #r[0].new(h.merge(macro: self)) if r
716
+ object_create(r[0], h.merge(macro: self)) if r
269
717
 
270
718
  end
271
719
 
@@ -273,7 +721,8 @@ class Macro
273
721
 
274
722
  action = e.text.strip
275
723
  r = ap.find_action action
276
- r[0].new(r[1]) if r
724
+ #r[0].new(r[1]) if r
725
+ object_create(r[0],r[1]) if r
277
726
 
278
727
  end
279
728
 
@@ -286,9 +735,7 @@ class Macro
286
735
  r = cp.find_constraint e.text
287
736
  puts 'found constraint ' + r.inspect if @debug
288
737
 
289
- if r then
290
- r[0].new(r[1])
291
- end
738
+ object_create(r[0], r[1]) if r
292
739
 
293
740
  end
294
741
 
@@ -333,6 +780,27 @@ class Macro
333
780
  def set_env()
334
781
  @triggers.each(&:set_env)
335
782
  end
783
+
784
+ def set_var(label, v='')
785
+
786
+ value = if v.to_f.to_s == v
787
+ v.to_f
788
+ elsif v.downcase == 'true'
789
+ true
790
+ elsif v.downcase == 'false'
791
+ false
792
+ elsif v.to_i.to_s == v
793
+ v.to_i
794
+ else
795
+ v
796
+ end
797
+
798
+ if not @local_variables.has_key? label.to_sym then
799
+ @local_variables.merge!({label.to_sym => value})
800
+ end
801
+
802
+ varify(label, value)
803
+ end
336
804
 
337
805
  def to_pc()
338
806
 
@@ -378,13 +846,21 @@ EOF
378
846
  puts 'before triggers' if @debug
379
847
 
380
848
  a << @triggers.map do |x|
849
+
850
+ puts 'x: ' + x.inspect if @debug
851
+ raise 'Macro#to_s trigger cannot be nil' if x.nil?
852
+
381
853
  s =-x.to_s(colour: colour)
854
+ puts 's: ' + s.inspect if @debug
382
855
 
383
856
  s2 = if s.lines.length > 1 then
384
857
  "\n" + s.lines.map {|x| x.prepend (' ' * (indent+1)) }.join
385
858
  else
386
859
  ' ' + s
387
860
  end
861
+
862
+ puts 's2: ' + s2.inspect if @debug
863
+
388
864
  #s.lines > 1 ? "\n" + x : x
389
865
  (colour ? "t".bg_red.gray.bold : 't') + ":" + s2
390
866
  end.join("\n")
@@ -519,42 +995,47 @@ EOF
519
995
 
520
996
  puts ('inside object h:' + h.inspect).debug if @debug
521
997
  klass = Object.const_get h[:class_type]
522
- puts klass.inspect.highlight if $debug
998
+ puts klass.inspect.highlight if @debug
523
999
 
524
1000
  if klass == GeofenceTrigger then
525
- puts 'GeofenceTrigger found'.highlight if $debug
1001
+ puts 'GeofenceTrigger found'.highlight if @debug
526
1002
  GeofenceTrigger.new(h, geofences: @geofences)
527
1003
  else
528
- puts 'before klass'
1004
+ puts 'before klass' if @debug
529
1005
  h2 = h.merge( macro: self)
530
- puts 'h2: ' + h2.inspect
1006
+ puts 'h2: ' + h2.inspect if @debug
531
1007
  r = klass.new h2
532
- puts 'r:' + r.inspect
1008
+ puts 'r:' + r.inspect if @debug
533
1009
  r
534
1010
 
535
1011
  end
536
1012
 
537
1013
  end
538
1014
 
539
- def varify(local_variables)
540
-
541
-
542
- local_variables.map do |key, value|
543
-
544
-
545
- type = VAR_TYPES[value.class.to_s.to_sym]
546
-
547
- h = {
548
- boolean_value: false,
549
- decimal_value: 0.0,
550
- int_value: 0,
551
- name: key,
552
- string_value: '',
553
- type: type[0]
554
- }
555
- h[type[1]] = value
556
- h
1015
+ def object_create(klass, *args)
1016
+
1017
+ begin
1018
+ klass.new(*args)
1019
+ rescue
1020
+ raise MacroError, klass.to_s + ': ' + ($!).to_s
557
1021
  end
1022
+ end
1023
+
1024
+ def varify(label, value='')
1025
+
1026
+
1027
+ type = VAR_TYPES[value.class.to_s.to_sym]
1028
+
1029
+ h = {
1030
+ boolean_value: false,
1031
+ decimal_value: 0.0,
1032
+ int_value: 0,
1033
+ name: label,
1034
+ string_value: '',
1035
+ type: type[0]
1036
+ }
1037
+ h[type[1]] = value
1038
+ h
558
1039
 
559
1040
  end
560
1041