macrohub 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ba95c07689c92cdb6df0081466aea3961da3eee30e7217f4d2077bca43984084
4
+ data.tar.gz: 3f860c4ba1baa27d5711b37a1443619d3c0418d27f0f3f64f7c8b204a5031ac1
5
+ SHA512:
6
+ metadata.gz: 6181da5d0d83f8a519ae29836a606376590a797fb97d8d3e0ac90cea8eec3bc9132703c6c39e09a2f9be2f675360115555c8be14b3bba3049323050e87fcd41c
7
+ data.tar.gz: 83b5c8505b963d25f55096fb59f9c5aaaad58cd3aea47f9ac46dbaea50baf05f278c526e157b600641b2f8c3acbb05f233e13e73aefd63b45f7f942ee3249edd
Binary file
Binary file
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # file: macrohub.rb
4
+
5
+ require 'rowx'
6
+ require 'app-routes'
7
+ require 'rxfhelper'
8
+ require 'chronic_between'
9
+
10
+ # This file includes the following classes:
11
+ #
12
+ # * SayAction
13
+ # * WebhookAction
14
+ # * MotionTrigger
15
+ # * FrequencyConstraint
16
+ # * TimeConstraint
17
+ # * TriggersNlp - selects the trigger to use
18
+ # * ActionsNlp - selects the action to use
19
+ # * ConstraintsNlp - selects the constraint to use
20
+ # * Macro - contains the triggers, actions, and constraints
21
+ # * MacroHub - contains the macros
22
+
23
+ class SayAction
24
+
25
+ def initialize(s=nil, msg: s)
26
+ @s = msg
27
+ end
28
+
29
+ def invoke()
30
+ "say: %s" % @s
31
+ end
32
+
33
+ def to_node()
34
+ Rexle::Element.new(:action, attributes: {type: :say, text: @s})
35
+ end
36
+
37
+ def to_rowx()
38
+ "action: say '%s'" % @s
39
+ end
40
+
41
+ end
42
+
43
+ class WebhookAction
44
+
45
+ attr_accessor :url
46
+
47
+ def initialize(idx=nil, id: idx, url: '127.0.0.1', env: {debug: false})
48
+
49
+ @name, @url = id, url
50
+
51
+ end
52
+
53
+ def invoke()
54
+ "webhook: %s" % @url
55
+ end
56
+
57
+ def to_node()
58
+ Rexle::Element.new(:action, \
59
+ attributes: {type: :webhook, name: @name, url: @url})
60
+ end
61
+
62
+ def to_rowx()
63
+ s = "action: webhook %s" % @name
64
+ s += "\n url: %s" % @url
65
+ end
66
+
67
+ end
68
+
69
+ class MotionTrigger
70
+
71
+ attr_reader :location, :type
72
+
73
+ def initialize(location: nil)
74
+
75
+ @location = location
76
+ @type = :motion
77
+
78
+ end
79
+
80
+ def match?(detail)
81
+
82
+ puts 'inside MotionTrigger#match' if $debug
83
+ location = detail[:location]
84
+
85
+ if location then
86
+
87
+ @location.downcase == location.downcase
88
+
89
+ else
90
+ return false
91
+ end
92
+
93
+ end
94
+
95
+ def to_node()
96
+ Rexle::Element.new(:trigger, attributes: {type: :motion,
97
+ location: @location})
98
+ end
99
+
100
+ def to_rowx()
101
+ "trigger: Motion detected in the %s" % @location
102
+ end
103
+
104
+ end
105
+
106
+ class FrequencyConstraint
107
+
108
+ def initialize(freqx, freq: freqx)
109
+
110
+ @freq = freq
111
+ @counter = 0
112
+ @interval = 60
113
+ end
114
+
115
+ def counter()
116
+ @counter
117
+ end
118
+
119
+ def increment()
120
+ @counter += 1
121
+ end
122
+
123
+ def match?()
124
+ @counter < @freq
125
+ end
126
+
127
+ def reset()
128
+ puts 'resetting' if $debug
129
+ @counter = 0
130
+ end
131
+
132
+ def to_node()
133
+ Rexle::Element.new(:constraint, \
134
+ attributes: {type: :frequency, freq: @freq})
135
+ end
136
+
137
+ def to_rowx()
138
+
139
+ freq = case @freq
140
+ when 1
141
+ 'Once'
142
+ when 2
143
+ 'Twice'
144
+ else
145
+ "Maximum %s times" % @freq
146
+ end
147
+
148
+ "constraint: %s" % freq
149
+
150
+ end
151
+
152
+ end
153
+
154
+ class TimeConstraint
155
+
156
+ attr_accessor :time
157
+
158
+ def initialize(timex=nil, times: timex, time: timex)
159
+
160
+ @time = times || time
161
+
162
+ end
163
+
164
+ def match?(detail)
165
+
166
+ if $debug then
167
+ puts 'inside TimeConstraint#match?'
168
+ puts 'detail: ' + detail.inspect
169
+ puts '@time: ' + @time.inspect
170
+ end
171
+
172
+ ChronicBetween.new(@time).within?(detail[:time])
173
+
174
+ end
175
+
176
+ def to_node()
177
+ Rexle::Element.new(:constraint, \
178
+ attributes: {type: :time, time: @time})
179
+ end
180
+
181
+ def to_rowx()
182
+ "constraint: %s" % @time
183
+ end
184
+
185
+ end
186
+
187
+
188
+
189
+
190
+ class TriggersNlp
191
+ include AppRoutes
192
+
193
+ attr_reader :to_type
194
+
195
+ def initialize()
196
+
197
+ super()
198
+
199
+ params = {}
200
+ puts 'inside Trigger'
201
+ puts 'params: ' + params.inspect
202
+ triggers(params)
203
+
204
+ end
205
+
206
+ protected
207
+
208
+ def triggers(params)
209
+
210
+ puts 'inside triggers'
211
+
212
+ # e.g. Motion detected in the kitchen
213
+ #
214
+ get /motion detected in the (.*)/i do |location|
215
+ puts 'motion detection trigger' if $debug
216
+ [MotionTrigger, {location: location}]
217
+ end
218
+
219
+ end
220
+
221
+ private
222
+
223
+ alias find_trigger run_route
224
+
225
+ end
226
+
227
+ class ActionsNlp
228
+ include AppRoutes
229
+
230
+ def initialize()
231
+
232
+ super()
233
+
234
+ params = {}
235
+ actions(params)
236
+
237
+ end
238
+
239
+ protected
240
+
241
+ def actions(params)
242
+
243
+ puts 'inside actions'
244
+ # e.g. Say 'Good morning'
245
+ #
246
+ get /say ['"]([^'"]+)/i do |s|
247
+ puts 's: ' + s.inspect if $debug
248
+ [SayAction, {msg: s} ]
249
+ end
250
+
251
+ # e.g. webhook entered_kitchen
252
+ #
253
+ get /webhook (.*)/i do |name|
254
+ [WebhookAction, {id: name }]
255
+ end
256
+
257
+ get /.*/ do
258
+ puts 'action unknown' if $debug
259
+ []
260
+ end
261
+
262
+ end
263
+
264
+ private
265
+
266
+ alias find_action run_route
267
+
268
+ end
269
+
270
+ class ConstraintsNlp
271
+ include AppRoutes
272
+
273
+
274
+ def initialize()
275
+
276
+ super()
277
+
278
+ params = {}
279
+ constraints(params)
280
+
281
+ end
282
+
283
+ protected
284
+
285
+ def constraints(params)
286
+
287
+ puts 'inside constraints' if $debug
288
+ # e.g. Between 8am and 10am
289
+ #
290
+ get /^between (.*)/i do |s|
291
+ [TimeConstraint, {time: s}]
292
+ end
293
+
294
+ get /^on a (.*)/i do |s|
295
+ [TimeConstraint, {time: s}]
296
+ end
297
+
298
+ get /^(after .*)/i do |s|
299
+ [TimeConstraint, {time: s}]
300
+ end
301
+
302
+ get /^(#{(Date::DAYNAMES + Date::ABBR_DAYNAMES).join('|')}$)/i do |s|
303
+ [TimeConstraint, {time: s}]
304
+ end
305
+
306
+ get /^once only|only once|once|one time|1 time$/i do |s|
307
+ [FrequencyConstraint, {freq: 1}]
308
+ end
309
+
310
+ get /^twice only|only twice|twice|two times|2 times$/i do |s|
311
+ [FrequencyConstraint, {freq: 2}]
312
+ end
313
+
314
+ get /^(Maximum|Max|Up to) ?three times|3 times$/i do |s|
315
+ [FrequencyConstraint, {freq: 3}]
316
+ end
317
+
318
+ get /^(Maximum|Max|Up to) ?four times|4 times$/i do |s|
319
+ [FrequencyConstraint, {freq: 4}]
320
+ end
321
+
322
+ end
323
+
324
+ private
325
+
326
+ alias find_constraint run_route
327
+ end
328
+
329
+ class Macro
330
+
331
+ attr_accessor :title, :env
332
+ attr_reader :triggers, :actions, :constraints
333
+
334
+ def initialize(node, title: '')
335
+
336
+ @title = title
337
+
338
+ @actions = []
339
+ @triggers = []
340
+ @constraints = []
341
+
342
+ end
343
+
344
+ def import_xml(node)
345
+
346
+ @title = node.text('macro')
347
+
348
+ if node.element('triggers') then
349
+
350
+ triggers = {motion: MotionTrigger, timer: TimerTrigger}
351
+
352
+ # level 2
353
+ @triggers = node.xpath('triggers/*').map do |e|
354
+
355
+ puts 'e.name: ' + e.name.inspect if $debug
356
+ triggers[e.name.to_sym].new(e.attributes.to_h)
357
+
358
+ end
359
+
360
+ actions = {say: SayAction, webhook: WebhookAction}
361
+
362
+ @actions = node.xpath('actions/*').map do |e|
363
+
364
+ actions[e.name.to_sym].new(e.attributes.to_h)
365
+
366
+ end
367
+
368
+ constraints = {time: TimeConstraint, frequency: FrequencyConstraint}
369
+
370
+ @constraints = node.xpath('constraints/*').map do |e|
371
+
372
+ puts 'before Constraints.new' if $debug
373
+ constraints[e.name.to_sym].new(e.attributes.to_h)
374
+
375
+ end
376
+
377
+ else
378
+
379
+ # Level 1
380
+
381
+ tp = TriggersNlp.new
382
+
383
+ @triggers = node.xpath('trigger').map do |e|
384
+
385
+ r = tp.find_trigger e.text
386
+
387
+ if r then
388
+ r[0].new(r[1])
389
+ end
390
+
391
+ end
392
+
393
+ ap = ActionsNlp.new
394
+
395
+ @actions = node.xpath('action').map do |e|
396
+
397
+ r = ap.find_action e.text
398
+
399
+ if r then
400
+
401
+ a = e.xpath('item/*')
402
+
403
+ h = if a.any? then
404
+ a.map {|node| [node.name.to_sym, node.text.to_s]}.to_h
405
+ else
406
+ {}
407
+ end
408
+
409
+ r[0].new(r[1].merge(h))
410
+ end
411
+
412
+ end
413
+
414
+ cn = ConstraintsNlp.new
415
+
416
+ @constraints = node.xpath('constraint').map do |e|
417
+
418
+ puts 'constraint e: ' + e.xml.inspect
419
+ r = cn.find_constraint e.text
420
+
421
+ puts 'r: ' + r.inspect if $debug
422
+
423
+ if r then
424
+ r[0].new(r[1])
425
+ end
426
+
427
+ end
428
+
429
+ end
430
+ end
431
+
432
+ def match?(triggerx, detail={} )
433
+
434
+ if @triggers.any? {|x| x.type == triggerx and x.match?(detail) } then
435
+
436
+ if $debug then
437
+ puts 'checking constraints ...'
438
+ puts '@constraints: ' + @constraints.inspect
439
+ end
440
+
441
+ if @constraints.all? {|x| x.match?($env.merge(detail)) } then
442
+
443
+ true
444
+
445
+ else
446
+
447
+ return false
448
+
449
+ end
450
+
451
+ end
452
+
453
+ end
454
+
455
+ def run()
456
+ @actions.map(&:invoke)
457
+ end
458
+
459
+ def to_node()
460
+
461
+ if $debug then
462
+ puts 'inside to_node'
463
+ puts '@title: ' + @title.inspect
464
+ end
465
+
466
+ e = Rexle::Element.new(:macro, attributes: {title: @title})
467
+
468
+ e.add node_collection(:triggers, @triggers)
469
+ e.add node_collection(:actions, @actions)
470
+ e.add node_collection(:constraints, @constraints)
471
+
472
+ return e
473
+ end
474
+
475
+ def to_rowx()
476
+
477
+ s = "macro: %s\n\n" % @title
478
+ s + [@triggers, @actions, @constraints]\
479
+ .map {|x| x.collect(&:to_rowx).join("\n")}.join("\n")
480
+ end
481
+
482
+ private
483
+
484
+ def node_collection(name, a)
485
+
486
+ if $debug then
487
+ puts 'inside node_collection name: ' + name.inspect
488
+ puts 'a: ' + a.inspect
489
+ end
490
+
491
+ e = Rexle::Element.new(name)
492
+ a.each do |x|
493
+
494
+ puts 'x: ' + x.inspect if $debug
495
+ e.add x.to_node
496
+
497
+ end
498
+
499
+ return e
500
+
501
+ end
502
+
503
+ end
504
+
505
+ class MacroHub
506
+
507
+ attr_reader :macros
508
+
509
+ def initialize(obj=nil)
510
+
511
+ if obj then
512
+
513
+ s, _ = RXFHelper.read(obj)
514
+
515
+ if s[0] == '<'
516
+ import_xml(s)
517
+ else
518
+ import_xml(RowX.new(s.gsub(/^#.*/,'')).to_xml)
519
+ end
520
+
521
+ else
522
+
523
+ @macros = []
524
+
525
+ end
526
+ end
527
+
528
+ def import_xml(raws)
529
+
530
+ s = RXFHelper.read(raws).first
531
+ puts 's: ' + s.inspect if $debug
532
+ doc = Rexle.new(s)
533
+ puts 'after doc' if $debug
534
+
535
+ @macros = doc.root.xpath('item').map do |node|
536
+
537
+ macro = Macro.new node.text('macro')
538
+ macro.import_xml(node)
539
+ macro
540
+
541
+ end
542
+
543
+ end
544
+
545
+ def to_doc()
546
+
547
+ doc = Rexle.new('<macros/>')
548
+
549
+ @macros.each do |macro|
550
+ puts 'macro: ' + macro.inspect if $debug
551
+ doc.root.add macro.to_node
552
+ end
553
+
554
+ return doc
555
+
556
+ end
557
+
558
+ def to_rowx()
559
+
560
+ s = ''
561
+ s += "title: %s\n%s\n\n" % [@title, '-' * 50] if @title
562
+ s += @macros.collect(&:to_rowx).join("\n\n#{'-'*50}\n\n")
563
+
564
+ end
565
+
566
+ alias to_s to_rowx
567
+
568
+ def to_xml()
569
+ to_doc.xml pretty: true
570
+ end
571
+
572
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: macrohub
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Robertson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEXjCCAsagAwIBAgIBATANBgkqhkiG9w0BAQsFADAsMSowKAYDVQQDDCFnZW1t
14
+ YXN0ZXIvREM9amFtZXNyb2JlcnRzb24vREM9ZXUwHhcNMjAwODE4MjAzMzEwWhcN
15
+ MjEwODE4MjAzMzEwWjAsMSowKAYDVQQDDCFnZW1tYXN0ZXIvREM9amFtZXNyb2Jl
16
+ cnRzb24vREM9ZXUwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCpxMPq
17
+ 78/MXnTnNIR4r4w8nLvnmiI4+FQdKXM3B+4DtwOD9IYSoJvA0HvEAWaVRpCTNVqf
18
+ PofbiC3gtiN7fxr8TnuAoY45nm30E7iqOFZvRuHK3Ss9Lrw706yA0532QT3dctzU
19
+ /QpwjYTOxa1CPH2K0mudFJmgzthY22z3BSi8mgOCEcfXGkh2xmTXzxg8yIp62I5T
20
+ PzdH43Jpfh7goclqaMZqpg7C06vXpFfQJcy+AMdiSPgrowuJ0WIMK3aQof7lfFPa
21
+ f+l1rP2eJE/DGPwFDYNqFp5hgYk2N7WiSlp7C7rL/d7KclvXy7lVwOj0uO3MUQPe
22
+ a5Tyx+9KnXwFFGbi1q3IdrECfGnNeo70jd0mMjbSfeFPlRiALXKI4Sy3Gp1oNhsI
23
+ sw3epDAap/6TMaqaJbCHHxqk232BX56brcq71v/tJ1QLtK4kss5zir2S7WtT8EfF
24
+ Csr2KXHBn4yVQlTTvmTQohB2kTkuCQkvYLEtNhH3eJAGPufH83jz2kaW70cCAwEA
25
+ AaOBijCBhzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUVfxui/u3
26
+ RFnJbdwUpdKC7VGDNXgwJgYDVR0RBB8wHYEbZ2VtbWFzdGVyQGphbWVzcm9iZXJ0
27
+ c29uLmV1MCYGA1UdEgQfMB2BG2dlbW1hc3RlckBqYW1lc3JvYmVydHNvbi5ldTAN
28
+ BgkqhkiG9w0BAQsFAAOCAYEAphrYf3LYTmBQtbFJs5w4GgcBNQWSsLrUdeQKP+zy
29
+ KIVH03WEZm3A0Z5bFBYcLUu7ovp18w6fRIf3PaLk11vVrj2TzuwosT0VoNpeLKQk
30
+ HITIJkITbee6UFOmg7bvXiwQRNEbiaFhvNid+hIBg93TgbcaiVaXTssRb7diSU29
31
+ CNgBgY3DuwiS4YtcKim7T5OMqgw6IxvHR86MiE2S9DC36WTxb6bztJs6UCfboecM
32
+ 1GPnqPHri4Gbe9Nx1n0Y/pnQeXUAeetKisk1ry/TeQAbIxSGm7i0xqGFNWw3n6I5
33
+ KDpDkFCLQnCiyY8s11MYeaU6IsEJtnS7gth+tswkSXo83vwlFzsd2qrGyvFTtdoD
34
+ v98JCJNEFoLYGkFaAE+AOUJLcRANqZGgsBF5pRg91h0/U92CIMqAP3lAJcudEm7v
35
+ 3fO1S1r6TwHl52rWjJPDVGgq1Xy+wZgNc33Y5Mps7yaLvVH+GmCtOwmn2WNJ9sxH
36
+ K6cWFZZoUVgD1xtNkydgCYhq
37
+ -----END CERTIFICATE-----
38
+ date: 2020-08-18 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: rowx
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.7.0
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.7'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 0.7.0
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.7'
60
+ - !ruby/object:Gem::Dependency
61
+ name: app-routes
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.1'
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.19
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.1'
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.1.19
80
+ - !ruby/object:Gem::Dependency
81
+ name: rxfhelper
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.0.0
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.0
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: chronic_between
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 0.4.0
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.4'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.4.0
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '0.4'
120
+ description:
121
+ email: james@jamesrobertson.eu
122
+ executables: []
123
+ extensions: []
124
+ extra_rdoc_files: []
125
+ files:
126
+ - lib/macrohub.rb
127
+ homepage: https://github.com/jrobertson/macrohub
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.0.3
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Experimental gem to create macros with the aim to simulate home automation.
150
+ test_files: []
Binary file