macrohub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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