-
Notifications
You must be signed in to change notification settings - Fork 11
/
EventRunner4Engine.lua
2381 lines (2209 loc) · 108 KB
/
EventRunner4Engine.lua
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
E_VERSION,E_FIX = 0.5,"fix80"
--local _debugFlags = { triggers = true, post=true, rule=true, fcall=true }
-- _debugFlags = { fcall=true, triggers=true, post = true, rule=true }
Util = nil
triggerInterval = 500
--[[ Supported events:
Supported events:
{type='alarm', property='armed', id=<id>, value=<value>}
{type='alarm', property='breached', id=<id>, value=<value>}
{type='alarm', property='homeArmed', value=<value>}
{type='alarm', property='homeBreached', value=<value>}
{type='weather', property=<prop>, value=<value>, old=<value>}
{type='global-variable', property=<name>, value=<value>, old=<value>}
{type='device', id=<id>, property=<property>, value=<value>, old=<value>}
{type='device', id=<id>, property='centralSceneEvent', value={keyId=<value>, keyAttribute=<value>}}
{type='device', id=<id>, property='accessControlEvent', value=<value>}
{type='device', id=<id>, property='sceneACtivationEvent', value=<value>}
{type='profile', property='activeProfile', value=<value>, old=<value>}
{type='custom-event', name=<name>}
{type='updateReadyEvent', value=_}
{type='deviceEvent', id=<id>, value='removed'}
{type='deviceEvent', id=<id>, value='changedRoom'}
{type='deviceEvent', id=<id>, value='created'}
{type='deviceEvent', id=<id>, value='modified'}
{type='deviceEvent', id=<id>, value='crashed', error=<string>}
{type='sceneEvent', id=<id>, value='started'}
{type='sceneEvent', id=<id>, value='finished'}
{type='sceneEvent', id=<id>, value='instance', instance=d}
{type='sceneEvent', id=<id>, value='removed'}
{type='onlineEvent', value=<bool>}
Missing
{type='location', property='id', id=<number>, value=<string>}
{type='se-start', property='start', value='true'}
{type='climate', ...}
New functions:
self:profileId(name) -- returns id of profile with name
self:profileName(id) -- returns name of profile with id
self:activeProfile([id]) -- activates profile id. If id==nil return active profile.
self:getCustomEvent(name) -- return userDescription field of customEvent
self:postCustomEvent(name[,descr]) -- post existing customEvent (descr==nil), or creates and post customEvent (descr~=nil)
http.get(url,options) -- syncronous versions of http commands, only inside eventscript
http.put(url,options,data) --
http.post(url,options,data) --
http.delete(url,options)
--]]
--function QuickApp:main() -- EventScript version
-- local rule = function(...) return self:evalScript(...) end -- old rule function
-- self:enableTriggerType({"device","global-variable","custom-event"}) -- types of events we want
-- HT = {
-- keyfob = 26,
-- motion= 21,
-- temp = 22,
-- lux = 23,
-- }
-- Util.defvars(HT)
-- Util.reverseMapDef(HT)
-- rule("@@00:00:05 => log(now % 2 == 1 & 'Tick' | 'Tock')")
-- rule("keyfob:central => log('Key:%s',env.event.value.keyId)")
-- rule("motion:value => log('Motion:%s',motion:value)")
-- rule("temp:temp => log('Temp:%s',temp:temp)")
-- rule("lux:lux => log('Lux:%s',lux:lux)")
-- rule("wait(3); log('Res:%s',http.get('https://jsonplaceholder.typicode.com/todos/1').data)")
-- Nodered.connect("http://192.168.1.50:1880/ER_HC3")
-- rule("Nodered.post({type='echo1',value=42})")
-- rule("#echo1 => log('ECHO:%s',env.event.value)")
-- rule("log('Synchronous call:%s',Nodered.post({type='echo1',value=42},true))")
-- rule("#alarm{property='armed', value=true, id='$id'} => log('Zone %d armed',id)")
-- rule("#alarm{property='armed', value=false, id='$id'} => log('Zone %d disarmed',id)")
-- rule("#alarm{property='homeArmed', value=true} => log('Home armed')")
-- rule("#alarm{property='homeArmed', value=false} => log('Home disarmed')")
-- rule("#alarm{property='homeBreached', value=true} => log('Home breached')")
-- rule("#alarm{property='homeBreached', value=false} => log('Home safe')")
-- rule("#weather{property='$prop', value='$val'} => log('%s = %s',prop,val)")
-- rule("#profile{property='activeProfile', value='$val'} => log('New profile:%s',profile.name(val))")
-- rule("log('Current profile:%s',QA:profileName(QA:activeProfile()))")
-- rule("#customevent{name='$name'} => log('Custom event:%s',name)")
-- rule("#myBroadcast{value='$value'} => log('My broadcast:%s',value)")
-- rule("wait(5); QA:postCustomEvent('myEvent','this is a test')")
-- rule("wait(7); broadcast({type='myBroadcast',value=42})")
-- rule("#deviceEvent{id='$id',value='$value'} => log('Device %s %s',id,value)")
-- rule("#sceneEvent{id='$id',value='$value'} => log('Scene %s %s',id,value)")
-- dofile("verifyHC3scripts.lua")
--end
------------------- EventSupport - Don't change! --------------------
Toolbox_Module = Toolbox_Module or {}
local Module = Toolbox_Module
local _MARSHALL = true
local format = string.format
----------------- Module objects support -----------------------
Module.objects = { name="ER Object manager", version="0.1"}
function Module.objects.init()
if Module.objects.inited then return Module.objects.inited end
Module.objects.inited = true
-- TBD
return self
end
----------------- Module device support -----------------------
Module.device = { name="ER Device", version="0.2"}
function Module.device.init(self)
if Module.device.inited then return Module.device.inited end
Module.device.inited = true
local dev = { deviceID = self.id }
function self:UIHandler(event)
local obj = self
if self.id ~= event.deviceId then obj = (self.childDevices or {})[event.deviceId] end
if not obj then return end
local elm,etyp = event.elementName, event.eventType
local cb = obj.uiCallbacks or {}
if obj[elm] then return obj:callAction(elm, event) end
if cb[elm] and cb[elm][etyp] and self[cb[elm][etyp]] then return obj:callAction(cb[elm][etyp], event) end
if obj[elm.."Clicked"] then return obj:callAction(elm.."Clicked", event) end
if self.EM then
self:post({type='UI',id=event.deviceId,name=event.elementName,event=event.eventType,value=event.values})
else
self:warning("UI callback for element:", elm, " not found.")
end
end
-- Patch fibaro.call to track manual switches
local lastID,switchMap = {},{}
local oldFibaroCall = fibaro.call
function fibaro.call(id,action,...)
if ({turnOff=true,turnOn=true,on=true,toggle=true,off=true,setValue=true})[action] then lastID[id]={script=true,time=os.time()} end
if action=='setValue' and switchMap[id]==nil then
local actions = (__fibaro_get_device(id) or {}).actions or {}
switchMap[id] = actions.turnOff and not actions.setValue
end
if switchMap[id] then action=({...})[1] and 'turnOn' or 'turnOff' end
return oldFibaroCall(id,action,...)
end
local function lastHandler(ev)
if ev.type=='device' and ev.property=='value' then
local last = lastID[ev.id]
local _,t = fibaro.get(ev.id,'value')
--if last and last.script then print("T:"..(t-last.time)) end
if not(last and last.script and t-last.time <= 2) then
lastID[ev.id]={script=false, time=t}
end
end
end
self._Events.addEventHandler(lastHandler)
function self:lastManual(id)
local last = lastID[id]
if not last then return -1 end
return last.script and -1 or os.time()-last.time
end
return dev
end
----------------- Module utilities ----------------------------
Module.utilities = { name="ER Utilities", version="0.6"}
function Module.utilities.init()
if Module.utilities.inited then return Module.utilities.inited end
Module.utilities.inited = true
local self,format,QA = {},string.format,quickApp
local midnight,hm2sec,toTime,transform,copy,equal=QA.EM.midnight,QA.EM.hm2sec,QA.EM.toTime,QA.EM.transform,QA.EM.copy,QA.EM.equal
function self.findEqual(tab,obj)
for _,o in ipairs(tab) do if equal(o,obj) then return true end end
end
if not table.maxn then
function table.maxn(tbl)
local c=0
for _ in pairs(tbl) do c=c+1 end
return c
end
end
function self.map(f,l,s) s = s or 1; local r={} for i=s,table.maxn(l) do r[#r+1] = f(l[i]) end return r end
function self.mapAnd(f,l,s) s = s or 1; local e=true for i=s,table.maxn(l) do e = f(l[i]) if not e then return false end end return e end
function self.mapOr(f,l,s) s = s or 1; for i=s,table.maxn(l) do local e = f(l[i]) if e then return e end end return false end
function self.mapF(f,l,s) s = s or 1; local e=true for i=s,table.maxn(l) do e = f(l[i]) end return e end
function self.mapkl(f,l) local r={} for i,j in pairs(l) do r[#r+1]=f(i,j) end return r end
function self.mapkk(f,l) local r={} for k,v in pairs(l) do r[k]=f(v) end return r end
function self.member(v,tab) for _,e in ipairs(tab) do if v==e then return e end end return nil end
function self.append(t1,t2) for _,e in ipairs(t2) do t1[#t1+1]=e end return t1 end
function isError(e) return type(e)=='table' and e.ERR end
function throwError(args) args.ERR=true; error(args,args.level) end
function self.mkStream(tab)
local p,self=0,{ stream=tab, eof={type='eof', value='', from=tab[#tab].from, to=tab[#tab].to} }
function self.next() p=p+1 return p<=#tab and tab[p] or self.eof end
function self.last() return tab[p] or self.eof end
function self.peek(n) return tab[p+(n or 1)] or self.eof end
return self
end
function self.mkStack()
local p,st,self=0,{},{}
function self.push(v) p=p+1 st[p]=v end
function self.pop(n) n = n or 1; p=p-n; return st[p+n] end
function self.popn(n,v) v = v or {}; if n > 0 then local p = self.pop(); self.popn(n-1,v); v[#v+1]=p end return v end
function self.peek(n) return st[p-(n or 0)] end
function self.lift(n) local s = {} for i=1,n do s[i] = st[p-n+i] end self.pop(n) return s end
function self.liftc(n) local s = {} for i=1,n do s[i] = st[p-n+i] end return s end
function self.isEmpty() return p<=0 end
function self.size() return p end
function self.setSize(np) p=np end
function self.set(i,v) st[p+i]=v end
function self.get(i) return st[p+i] end
function self.dump() for i=1,p do print(json.encode(st[i])) end end
function self.clear() p,st=0,{} end
return self
end
self._vars = {}
local _vars = self._vars
local _triggerVars = {}
self._triggerVars = _triggerVars
self._reverseVarTable = {}
function self.defvar(var,expr) if _vars[var] then _vars[var][1]=expr else _vars[var]={expr} end end
function self.defvars(tab) for var,val in pairs(tab) do self.defvar(var,val) end end
function self.defTriggerVar(var,expr) _triggerVars[var]=true; self.defvar(var,expr) end
function self.triggerVar(v) return _triggerVars[v] end
function self.reverseMapDef(table) self._reverseMap({},table) end
function self._reverseMap(path,value)
if type(value) == 'number' then self._reverseVarTable[tostring(value)] = table.concat(path,".")
elseif type(value) == 'table' and not value[1] then
for k,v in pairs(value) do table.insert(path,k); self._reverseMap(path,v); table.remove(path) end
end
end
function self.reverseVar(id) return Util._reverseVarTable[tostring(id)] or id end
local function isVar(v) return type(v)=='table' and v[1]=='%var' end
self.isVar = isVar
function self.isGlob(v) return isVar(v) and v[3]=='glob' end
self.coroutine = {
create = function(code,src,env)
env=env or {}
env.cp,env.stack,env.code,env.src=1,Util.mkStack(),code,src
return {state='suspended', context=env}
end,
resume = function(co)
if co.state=='dead' then return false,"cannot resume dead coroutine" end
if co.state=='running' then return false,"cannot resume running coroutine" end
co.state='running'
local status = {pcall(Rule.ScriptEngine.eval,co.context)}
if status[1]==false then return status[2] end
co.state= status[2]=='suspended' and status[2] or 'dead'
return true,table.unpack(status[3])
end,
status = function(co) return co.state end,
_reset = function(co) co.state,co.context.cp='suspended',1; co.context.stack.clear(); return co.context end
}
local VIRTUALDEVICES = {}
function self.defineVirtualDevice(id,call,get) VIRTUALDEVICES[id]={call=call,get=get} end
do
local oldGet,oldCall = fibaro.get,fibaro.call
function fibaro.call(id,action,...) local d = VIRTUALDEVICES[id]
if d and d.call and d.call(id,action,...) then return
else oldCall(id,action,...) end
end
function fibaro.get(id,prop,...) local g = VIRTUALDEVICES[id]
if g and g.get then
local stat,res = g.get(id,prop,...)
if stat then return table.unpack(res) end
end
return oldGet(id,prop,...)
end
end
local NOFTRACE={
[""]=true,["ER_remoteEvent"]=true,
['SUBSCRIBEDEVENT']=true,
['SYNCPUBSUB']=true,
['SUBSCRIBEDEVENT']=true
}
local function patchF(name)
local oldF,flag = fibaro[name],"f"..name
fibaro[name] = function(...)
local args = {...}
local res = {oldF(...)}
if _debugFlags[flag] then
if not NOFTRACE[args[2] or ""] then
args = #args==0 and "" or json.encode(args):sub(2,-2)
pdebug("fibaro.%s(%s) => %s",name,args,#res==0 and "nil" or #res==1 and res[1] or res)
end
end
return table.unpack(res)
end
end
patchF("call")
function urldecode(str) return str:gsub('%%(%x%x)',function (x) return string.char(tonumber(x,16)) end) end
function split(s, sep)
local fields = {}
sep = sep or " "
local pattern = string.format("([^%s]+)", sep)
string.gsub(s, pattern, function(c) fields[#fields + 1] = c end)
return fields
end
function self.gensym(s) return (s or "G")..QA._orgToString({}):match("%s(.*)") end
function self.makeBanner(str)
if #str % 2 == 1 then str=str.." " end
local n = #str+2
local l2=100/2-n/2
return string.rep("-",l2).." "..str.." "..string.rep("-",l2)
end
function self.printBanner(str) QA:debug(self.makeBanner(str)) end
function self.printColorAndTag(tag,color,fmt,...)
assert(tag and color and fmt,"print needs tag, color, and args")
local args={...}
if #args > 0 then
for i,v in ipairs(args) do if type(v)=='table' then args[i]=tostring(v) end end
fmt=string.format(fmt,table.unpack(args))
end
local t = __TAG
__TAG = tag or __TAG
if hc3_emulator or not color then quickApp:trace(fmt)
else
quickApp:trace("<font color="..color..">"..fmt.."</font>")
end
__TAG = t
end
function pdebug(...) return quickApp:debugf(...) end
function ptrace(...) return quickApp:tracef(...) end
function pwarning(...) return quickApp:warningf(...) end
function perror(...) return quickApp:errorf(...) end
function psys(...) return quickApp:tracef(...) end
function Debug(flag,...) if flag then quickApp:debugf(...) end end
function _assert(test,msg,...) if not test then error(string.format(msg,...),3) end end
function _assertf(test,msg,fun) if not test then error(string.format(msg,fun and fun() or ""),3) end end
local function time2str(t) return format("%02d:%02d:%02d",math.floor(t/3600),math.floor((t%3600)/60),t%60) end
local function between(t11,t22)
local t1,t2,tn = midnight()+hm2sec(t11),midnight()+hm2sec(t22),os.time()
if t1 <= t2 then return t1 <= tn and tn <= t2 else return tn <= t1 or tn >= t2 end
end
function toTime(time)
if type(time) == 'number' then return time end
local p = time:sub(1,2)
if p == '+/' then return hm2sec(time:sub(3))+os.time()
elseif p == 'n/' then
local t1,t2 = midnight()+hm2sec(time:sub(3)),os.time()
return t1 > t2 and t1 or t1+24*60*60
elseif p == 't/' then return hm2sec(time:sub(3))+midnight()
else return hm2sec(time) end
end
local cbr = {}
function self.asyncCall(errstr,timeout)
local tag, cbr = Util.gensym("CBR"),cbr
cbr[tag]={nil,nil,errstr}
cbr[tag][1]=setTimeout(function()
cbr[tag]=nil
perror("No response from %s call",errstr)
end,timeout)
return tag,{
['<cont>']=
function(cont)
cbr[tag][2]=cont
end
}
end
function self.receiveAsync(tag,res)
local cr = cbr[tag] or {}
if cr[1] then clearTimeout(cr[1]) end
if cr[2] then
local stat,res = pcall(function() cr[2](res) end)
if not stat then perror("Error in %s call - %s",cr[3],res) end
end
cbr[tag]=nil
end
local gKeys = {type=1,id=2,value=3,val=4,key=5,arg=6,event=7,events=8,msg=9,res=10}
local gKeysNext = 10
local function keyCompare(a,b)
local av,bv = gKeys[a], gKeys[b]
if av == nil then gKeysNext = gKeysNext+1 gKeys[a] = gKeysNext av = gKeysNext end
if bv == nil then gKeysNext = gKeysNext+1 gKeys[b] = gKeysNext bv = gKeysNext end
return av < bv
end
function self.prettyJson(e) -- our own json encode, as we don't have 'pure' json structs, and sorts keys in order
local res,seen = {},{}
local function pretty(e)
local t = type(e)
if t == 'string' then res[#res+1] = '"' res[#res+1] = e res[#res+1] = '"'
elseif t == 'number' then res[#res+1] = e
elseif t == 'boolean' or t == 'function' or t=='thread' then res[#res+1] = tostring(e)
elseif t == 'table' then
if next(e)==nil then res[#res+1]='{}'
elseif seen[e] then res[#res+1]="..rec.."
elseif e[1] or #e>0 then
seen[e]=true
res[#res+1] = "[" pretty(e[1])
for i=2,#e do res[#res+1] = "," pretty(e[i]) end
res[#res+1] = "]"
else
seen[e]=true
if e._var_ then res[#res+1] = format('"%s"',e._str) return end
local k = {} for key,_ in pairs(e) do k[#k+1] = key end
table.sort(k,keyCompare)
if #k == 0 then res[#res+1] = "[]" return end
res[#res+1] = '{'; res[#res+1] = '"' res[#res+1] = k[1]; res[#res+1] = '":' t = k[1] pretty(e[t])
for i=2,#k do
res[#res+1] = ',"' res[#res+1] = k[i]; res[#res+1] = '":' t = k[i] pretty(e[t])
end
res[#res+1] = '}'
end
elseif e == nil then res[#res+1]='null'
else error("bad json expr:"..tostring(e)) end
end
pretty(e)
return table.concat(res)
end
self.S1 = {click = "16", double = "14", tripple = "15", hold = "12", release = "13"}
self.S2 = {click = "26", double = "24", tripple = "25", hold = "22", release = "23"}
self.netSync = { HTTPClient = function (log)
local self,queue,HTTP,key = {},{},net.HTTPClient(),0
local _request
local function dequeue()
table.remove(queue,1)
local v = queue[1]
if v then
Debug(_debugFlags.netSync,"netSync:Pop %s (%s)",v[3],#queue)
--setTimeout(function() _request(table.unpack(v)) end,1)
_request(table.unpack(v))
end
end
_request = function(url,params,key)
params = copy(params)
local uerr,usucc = params.error,params.success
params.error = function(status)
Debug(_debugFlags.netSync,"netSync:Error %s %s",key,status)
dequeue()
if params._logErr then perror(" %s:%s",log or "netSync:",tojson(status)) end
if uerr then uerr(status) end
end
params.success = function(status)
Debug(_debugFlags.netSync,"netSync:Success %s",key)
dequeue()
if usucc then usucc(status) end
end
Debug(_debugFlags.netSync,"netSync:Calling %s",key)
HTTP:request(url,params)
end
function self:request(url,parameters)
key = key+1
if next(queue) == nil then
queue[1]='RUN'
_request(url,parameters,key)
else
Debug(_debugFlags.netSync,"netSync:Push %s",key)
queue[#queue+1]={url,parameters,key}
end
end
return self
end}
self.getWeekNumber = function(tm) return tonumber(os.date("%V",tm)) end
function self.dateTest(dateStr)
local days = {sun=1,mon=2,tue=3,wed=4,thu=5,fri=6,sat=7}
local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
local last,month = {31,28,31,30,31,30,31,31,30,31,30,31},nil
local function seq2map(seq) local s = {} for _,v in ipairs(seq) do s[v] = true end return s; end
local function flatten(seq,res) -- flattens a table of tables
res = res or {}
if type(seq) == 'table' then for _,v1 in ipairs(seq) do flatten(v1,res) end else res[#res+1] = seq end
return res
end
local function expandDate(w1,md)
local function resolve(id)
local res
if id == 'last' then month = md res=last[md]
elseif id == 'lastw' then month = md res=last[md]-6
else res= type(id) == 'number' and id or days[id] or months[id] or tonumber(id) end
_assert(res,"Bad date specifier '%s'",id) return res
end
local w,m,step= w1[1],w1[2],1
local start,stop = w:match("(%w+)%p(%w+)")
if (start == nil) then return resolve(w) end
start,stop = resolve(start), resolve(stop)
local res,res2 = {},{}
if w:find("/") then
if not w:find("-") then -- 10/2
step=stop; stop = m.max
else step=w:match("/(%d+)") end
end
step = tonumber(step)
_assert(start>=m.min and start<=m.max and stop>=m.min and stop<=m.max,"illegal date intervall")
while (start ~= stop) do -- 10-2
res[#res+1] = start
start = start+1; if start>m.max then start=m.min end
end
res[#res+1] = stop
if step > 1 then for i=1,#res,step do res2[#res2+1]=res[i] end; res=res2 end
return res
end
local function parseDateStr(dateStr,last)
local map = Util.map
local seq = split(dateStr," ") -- min,hour,day,month,wday
local lim = {{min=0,max=59},{min=0,max=23},{min=1,max=31},{min=1,max=12},{min=1,max=7}}
for i=1,5 do if seq[i]=='*' or seq[i]==nil then seq[i]=tostring(lim[i].min).."-"..lim[i].max end end
seq = map(function(w) return split(w,",") end, seq) -- split sequences "3,4"
local month = os.date("*t",os.time()).month
seq = map(function(t) local m = table.remove(lim,1);
return flatten(map(function (g) return expandDate({g,m},month) end, t))
end, seq) -- expand intervalls "3-5"
return map(seq2map,seq)
end
local sun,offs,day,sunPatch = dateStr:match("^(sun%a+) ([%+%-]?%d+)")
if sun then
sun = sun.."Hour"
dateStr=dateStr:gsub("sun%a+ [%+%-]?%d+","0 0")
sunPatch=function(dateSeq)
local h,m = (fibaro:getValue(1,sun)):match("(%d%d):(%d%d)")
dateSeq[1]={[(tonumber(h)*60+tonumber(m)+tonumber(offs))%60]=true}
dateSeq[2]={[math.floor((tonumber(h)*60+tonumber(m)+tonumber(offs))/60)]=true}
end
end
local dateSeq = parseDateStr(dateStr)
return function() -- Pretty efficient way of testing dates...
local t = os.date("*t",os.time())
if month and month~=t.month then parseDateStr(dateStr) end -- Recalculate 'last' every month
if sunPatch and (month and month~=t.month or day~=t.day) then sunPatch(dateSeq) day=t.day end -- Recalculate 'last' every month
return
dateSeq[1][t.min] and -- min 0-59
dateSeq[2][t.hour] and -- hour 0-23
dateSeq[3][t.day] and -- day 1-31
dateSeq[4][t.month] and -- month 1-12
dateSeq[5][t.wday] or false -- weekday 1-7, 1=sun, 7=sat
end
end
---- SunCalc -----
local function sunturnTime(date, rising, latitude, longitude, zenith, local_offset)
local rad,deg,floor = math.rad,math.deg,math.floor
local frac = function(n) return n - floor(n) end
local cos = function(d) return math.cos(rad(d)) end
local acos = function(d) return deg(math.acos(d)) end
local sin = function(d) return math.sin(rad(d)) end
local asin = function(d) return deg(math.asin(d)) end
local tan = function(d) return math.tan(rad(d)) end
local atan = function(d) return deg(math.atan(d)) end
local function day_of_year(date)
local n1 = floor(275 * date.month / 9)
local n2 = floor((date.month + 9) / 12)
local n3 = (1 + floor((date.year - 4 * floor(date.year / 4) + 2) / 3))
return n1 - (n2 * n3) + date.day - 30
end
local function fit_into_range(val, min, max)
local range,count = max - min
if val < min then count = floor((min - val) / range) + 1; return val + count * range
elseif val >= max then count = floor((val - max) / range) + 1; return val - count * range
else return val end
end
-- Convert the longitude to hour value and calculate an approximate time
local n,lng_hour,t = day_of_year(date), longitude / 15, nil
if rising then t = n + ((6 - lng_hour) / 24) -- Rising time is desired
else t = n + ((18 - lng_hour) / 24) end -- Setting time is desired
local M = (0.9856 * t) - 3.289 -- Calculate the Sun^s mean anomaly
-- Calculate the Sun^s true longitude
local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360)
-- Calculate the Sun^s right ascension
local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360)
-- Right ascension value needs to be in the same quadrant as L
local Lquadrant = floor(L / 90) * 90
local RAquadrant = floor(RA / 90) * 90
RA = RA + Lquadrant - RAquadrant; RA = RA / 15 -- Right ascension value needs to be converted into hours
local sinDec = 0.39782 * sin(L) -- Calculate the Sun's declination
local cosDec = cos(asin(sinDec))
local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) -- Calculate the Sun^s local hour angle
if rising and cosH > 1 then return "N/R" -- The sun never rises on this location on the specified date
elseif cosH < -1 then return "N/S" end -- The sun never sets on this location on the specified date
local H -- Finish calculating H and convert into hours
if rising then H = 360 - acos(cosH)
else H = acos(cosH) end
H = H / 15
local T = H + RA - (0.06571 * t) - 6.622 -- Calculate local mean time of rising/setting
local UT = fit_into_range(T - lng_hour, 0, 24) -- Adjust back to UTC
local LT = UT + local_offset -- Convert UT value to local time zone of latitude/longitude
return os.time({day = date.day,month = date.month,year = date.year,hour = floor(LT),min = math.modf(frac(LT) * 60)})
end
local function getTimezone() local now = os.time() return os.difftime(now, os.time(os.date("!*t", now))) end
function self.sunCalc(time)
local hc2Info = api.get("/settings/location") or {}
local lat = hc2Info.latitude
local lon = hc2Info.longitude
local utc = getTimezone() / 3600
local zenith,zenith_twilight = 90.83, 96.0 -- sunset/sunrise 90°50′, civil twilight 96°0′
local date = os.date("*t",time or os.time())
if date.isdst then utc = utc + 1 end
local rise_time = os.date("*t", sunturnTime(date, true, lat, lon, zenith, utc))
local set_time = os.date("*t", sunturnTime(date, false, lat, lon, zenith, utc))
local rise_time_t,set_time_t = rise_time,set_time
pcall(function()
rise_time_t = os.date("*t", sunturnTime(date, true, lat, lon, zenith_twilight, utc))
set_time_t = os.date("*t", sunturnTime(date, false, lat, lon, zenith_twilight, utc))
end)
local sunrise = format("%.2d:%.2d", rise_time.hour, rise_time.min)
local sunset = format("%.2d:%.2d", set_time.hour, set_time.min)
local sunrise_t = format("%.2d:%.2d", rise_time_t.hour, rise_time_t.min)
local sunset_t = format("%.2d:%.2d", set_time_t.hour, set_time_t.min)
return sunrise, sunset, sunrise_t, sunset_t
end
if not hc3_emulator then
_IPADDRESS = _HC3IPADDRESS
function self.getIPaddress()
if _IPADDRESS then return _IPADDRESS end
local nets = api.get("/settings/network").networkConfig or {}
if nets.wlan0.enabled then
_IPADDRESS = nets.wlan0.ipConfig.ip
elseif nets.eth0.enabled then
_IPADDRESS = nets.eth0.ipConfig.ip
else
error("Can't find IP address")
end
return _IPADDRESS
end
else
self.getIPaddress = hc3_emulator.getIPaddress
end
self.equal,self.copy,self.transform,self.toTime,self.hm2sec,self.midnight = equal,copy,transform,toTime,hm2sec,midnight
tojson,self.time2str,self.between = self.prettyJson,time2str,between
Util = self
return self
end -- Utils
----------------- Autopatch support ---------------------------
Module.autopatch = { name="ER Autopatch", version="0.2"}
function Module.autopatch.init(self)
if Module.autopatch.inited then return Module.autopatch.inited end
Module.autopatch.inited = true
local patchFiles = {
["EventRunner4Engine.lua"] = {
version = _version,
files = {
['EventRunner']="EventRunner4Engine.lua",
['Toolbox']="Toolbox/Toolbox_basic.lua",
['Toolbox_events']="Toolbox/Toolbox_events.lua",
['Toolbox_child']="Toolbox/Toolbox_child.lua",
['Toolbox_triggers']="Toolbox/Toolbox_triggers.lua",
['Toolbox_files']="Toolbox/Toolbox_files.lua",
['Toolbox_rpc']="Toolbox/Toolbox_rpc.lua",
['Toolbox_pubsub']="Toolbox/Toolbox_pubsub.lua",
}
},
}
local versionInfo = nil
__VERSION = "https://raw.githubusercontent.com/jangabrielsson/EventRunner/master/VERSION4.json"
function Util.checkForUpdates()
local req = net.HTTPClient()
req:request(__VERSION,{
options = {method = 'GET', checkCertificate = false, timeout=20000},
success=function(data)
if data.status == 200 then
versionInfo = json.decode(data.data)
for file,version in pairs(versionInfo or {}) do
if patchFiles[file] and patchFiles[file].version ~= version then
self:post({type='File_update',file=file,version=version, _sh=true})
end
end
end
end})
end
local function fetchFile(file,path,files,mn,cont)
local req = net.HTTPClient()
req:request("https://raw.githubusercontent.com/jangabrielsson/EventRunner/master/"..path,{
options = {method = 'GET', checkCertificate = false, timeout=20000},
success=function(data)
if data.status == 200 then
files[file]=data.data
local n = 0
for _,_ in pairs(files) do n=n+1 end
if n==mn then cont(files) end
end
end,
error=function(status) self:errorf("Get src code from Github: %s",status) end
})
return
end
function Util.updateFile(file)
local finfo = patchFiles[file]
assert(file,"PatchFile: No such file "..(file or "nil"))
local files = {}
local n = 0;
for _,_ in pairs(finfo.files) do n=n+1 end
local function patcher(nfiles)
---if hc3_emulator then return end -- not in emulator
local id,cfiles = self.id,{}
local of = self:getFiles(id)
for _,f in pairs(of) do
if not f.isMain then
local d = self:getFile(id,f.name)
if not(nfiles[f.name] and nfiles[f.name]==d.content) then
cfiles[f.name]=d.content
else nfiles[f.name]= nil end
end
end -- current files
local updates,adds,dels = {},{},{}
local updates_n,adds_n,dels_n = 0,0,0
for f,d in pairs(nfiles) do
if d~=cfiles[f] then -- different
if cfiles[f]== nil then adds[f]=d adds_n=adds_n+1 -- missing
else updates[f]=d updates_n=updates_n+1 end -- changed
end
end
for f,d in pairs(cfiles) do if not nfiles[f] then dels[f]=d dels_n=dels_n+1 end end
-- Files needing to update
self:debugf("%d files needs to be updated",updates_n)
self:debugf("%d files needs to be added",adds_n)
self:debugf("%d files needs to be deleted",dels_n)
for f,d in pairs(dels) do self:debugf("Deleting %s",f) self:deleteFile(id,f) end
for f,d in pairs(adds) do self:debugf("Adding %s",f) self:addFileTo(d,f,id) end
local ups = {}
for f,d in pairs(updates) do
self:debugf("Updating %s",f)
ups[#ups+1]={
name=f,
content=d,
isMain=false,
isOpen=false
}
end
if #ups > 0 then
self:updateFiles(id,ups)
end
end
for file,path in pairs(finfo.files) do fetchFile(file,path,files,n,patcher) end
end
function DOWNLOADSOURCE()
local function createDir(dir)
local r,err = hc3_emulator.file.make_dir(dir)
if not r and err~="File exists" then error(format("Can't create backup directory: %s (%s)",dir,err)) end
end
createDir("Toolbox")
local TP = "https://raw.githubusercontent.com/jangabrielsson/EventRunner/master/"
for _,f in ipairs(
{
"Toolbox_basic.lua",
"Toolbox_events.lua",
"Toolbox_child.lua",
"Toolbox_triggers.lua",
"Toolbox_files.lua",
"Toolbox_rpc.lua",
"Toolbox_pubsub.lua",
}
) do
hc3_emulator.file.downloadFile(TP.."Toolbox/"..f,"Toolbox/"..f)
end
hc3_emulator.file.downloadFile(TP.."EventRunner4Engine.lua","EventRunner4Engine.lua")
end
end
----------------- Module Extras -------------------------------
Module.extras = { name="ER Extras", version="0.2"}
function Module.extras.init(self)
if Module.extras.inited then return Module.extras.inited end
Module.extras.inited = true
-- Sunset/sunrise patch -- first time in the day someone asks for sunsethours we calculate and cache
local _SUNTIMEDAY = nil
local _SUNTIMEVALUES = {sunsetHour="00:00",sunriseHour="00:00",dawnHour="00:00",duskHour="00:00"}
Util.defineVirtualDevice(1,nil,function(id,prop,...)
if not _SUNTIMEVALUES[prop] then return nil end
local s = _SUNTIMEVALUES
local day = os.date("*t").day
if day ~= _SUNTIMEDAY then
_SUNTIMEDAY = day
s.sunriseHour,s.sunsetHour,s.dawnHour,s.duskHour=Util.sunCalc()
end
return true,{_SUNTIMEVALUES[prop],os.time()}
end)
local debugButtons = {
debugTrigger='Trigger',
debugPost='Post',
debugRule='Rule',
}
for b,n in pairs(debugButtons) do
local name = n..":"..(_debugFlags[n:lower()] and "ON" or "OFF")
self:updateView(b,"text",name)
end
self:event({type='UI'},
function(env)
local trigger = (debugButtons)[env.event.name]
if trigger then
local tname = trigger:lower()
_debugFlags[tname] = not _debugFlags[tname]
local name = trigger..":"..(_debugFlags[tname] and "ON" or "OFF")
self:updateView(env.event.name,"text",name)
return self._Events.BREAK
end
end)
class 'GenericChild'(QuickAppChild)
function GenericChild:__init(device)
QuickAppChild.__init(self,device)
self.eid = self:getVariable("eid")
function self:callAction(cmd,...)
local ev = {type='child',eid=self.eid,cmd=cmd,args={...}}
quickApp:post(ev)
end
end
local childMap = {}
function self:getChild(eid) return childMap[eid] end
function self:getChildren() return childMap end
function self:child(args)
assert(args and args.eid,"child missing eid")
for _,c in pairs(self.childDevices) do
if c.eid == args.eid then childMap[c.eid]=c return c end
end
local c = self:createChild{
type=args.type or "com.fibaro.binarySwitch",
name = args.name or "ER4 child",
quickVars = {eid = args.eid},
className = "GenericChild"
}
if c then childMap[args.eid]=c end
return c
end
function self:defineChildren(children)
local childs = {}
for id,dev in pairs(self.childDevices or {}) do childs[id]=dev end
for _,args in ipairs(children) do
local stat,res = pcall(function()
local c = self:child(args)
if c then childs[id]=nil end
end)
if not stat then self:error(res) end
end
for id,c in pairs(childs) do self:removeChildDevice(id) end -- Remove children not in list
end
function self:profileName(id) for _,p in ipairs(api.get("/profiles").profiles) do if p.id == id then return p.name end end end
function self:profileId(name) for _,p in ipairs(api.get("/profiles").profiles) do if p.name == name then return p.id end end end
function self:activeProfile(id)
if id then
if type(id)=='string' then id = profile.id(id) end
assert(id,"profile.active(id) - no such id/name")
return api.put("/profiles",{activeProfile=id}) and id
end
return api.get("/profiles").activeProfile
end
function self:postCustomEvent(name,descr)
if descr then
if api.get("/customEvents/"..name) then
api.put("/customEvents",{name=name,userDescription=descr})
else api.post("/customEvents",{name=name,userDescription=descr}) end
end
return fibaro.emitCustomEvent(name)
end
function self:getCustomEvent(name) return (api.get("customEvents/"..name) or {}).description end
function self:deleteCustomEvent(name) return api.delete("customEvents/"..name) end
Util.defvar('remote',function(id,event,time)
return self:post(function()
ptrace("Remote post to %d %s",id,event)
self:postRemote(id,event)
end,time)
end)
local function httpCall(url,options,data)
local opts = Util.copy(options)
opts.headers = opts.headers or {}
if opts.type then
opts.headers["content-type"]=opts.type
opts.type=nil
end
if not opts.headers["content-type"] then
opts.headers["content-type"] = 'application/json'
end
if opts.user or opts.pwd then
opts.headers['Authorization']= quickApp:basicAuthorization((opts.user or ""),(opts.pwd or ""))
opts.user,opts.pwd=nil,nil
end
opts.data = data and json.encode(data)
local tag,res = Util.asyncCall("HTTP",50000)
net.HTTPClient():request(url,{
options=opts,
success = function(res) Util.receiveAsync(tag,res) end,
error = function(res) Util.receiveAsync(tag,res) end
})
return res
end
local http = {}
function http.get(url,options) options=options or {}; options.method="GET" return httpCall(url,options) end
function http.put(url,options,data) options=options or {}; options.method="PUT" return httpCall(url,options,data) end
function http.post(url,options,data) options=options or {}; options.method="POST" return httpCall(url,options,data) end
function http.delete(url,options) options=options or {}; options.method="DELETE" return httpCall(url,options) end
Util.defvar("http",http)
Util.defvar("QA",self)
equations = {
linear = function(t, b, c, d) return c * t / d + b; end,
inQuad = function(t, b, c, d) t = t / d; return c * math.pow(t, 2) + b; end,
inOutQuad = function(t, b, c, d) t = t / d * 2; return t < 1 and c / 2 * math.pow(t, 2) + b or -c / 2 * ((t - 1) * (t - 3) - 1) + b end,
outInExpo = function(t, b, c, d) return t < d / 2 and equations.outExpo(t * 2, b, c / 2, d) or equations.inExpo((t * 2) - d, b + c / 2, c / 2, d) end,
inExpo = function(t, b, c, d) return t == 0 and b or c * math.pow(2, 10 * (t / d - 1)) + b - c * 0.001 end,
outExpo = function(t, b, c, d) return t == d and b + c or c * 1.001 * (-math.pow(2, -10 * t / d) + 1) + b end,
inOutExpo = function(t, b, c, d)
if t == 0 then return b elseif t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * math.pow(2, 10 * (t - 1)) + b - c * 0.0005 else t = t - 1; return c / 2 * 1.0005 * (-math.pow(2, -10 * t) + 2) + b end
end,
}
function Util.dimLight(id,sec,dir,step,curve,start,stop)
_assert(tonumber(sec), "Bad dim args for deviceID:%s",id)
local f = curve and equations[curve] or equations['linear']
dir,step = dir == 'down' and -1 or 1, step or 1
start,stop = start or 0,stop or 99
local t = dir == 1 and 0 or sec
self:post({type='%dimLight',id=id,sec=sec,dir=dir,fun=f,t=dir == 1 and 0 or sec,start=start,stop=stop,step=step,_sh=true})
end
self:event({type='%dimLight'},function(env)
local e = env.event
local ev,currV = e.v or -1,tonumber(fibaro.getValue(e.id,"value"))
if not currV then
self:warningf("Device %d can't be dimmed. Type of value is %s",e.id,type(fibaro.getValue(e.id,"value")))
end
if e.v and math.abs(currV - e.v) > 2 then return end -- Someone changed the lightning, stop dimming
e.v = math.floor(e.fun(e.t,e.start,e.stop,e.sec)+0.5)
if ev ~= e.v then fibaro.call(e.id,"setValue",e.v) end
e.t=e.t+e.dir*e.step
if 0 <= e.t and e.t <= e.sec then self:post(e,os.time()+e.step) end
end)
self:event({type='alarm', property='homeArmed'},
function(env) self:post({type='alarm',property='armed',id=0, value=env.event.value})
end)
self:event({type='alarm', property='homeBreached'},
function(env) self:post({type='alarm',property='breached',id=0, value=env.event.value})
end)
end
----------------- EventScript support -------------------------
Module.eventScript = { name="ER EventScript", version="0.7"}
function Module.eventScript.init()
if Module.eventScript.inited then return Module.eventScript.inited end
Module.eventScript.inited = true
local QA = quickApp
local ScriptParser,ScriptCompiler,ScriptEngine