forked from jangabrielsson/EventRunner
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEventRunner3.lua
3020 lines (2804 loc) · 139 KB
/
EventRunner3.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
--[[
%% properties
339 value
345 value
55 value
88 value
203 value
%% events
5 CentralSceneEvent
%% globals
TimeOfDay
HumidityBadStart
VarmeEffekt
%% autostart
--]]
if dofile and not _EMULATED then _EMULATED={name="EventRunner",id=99,maxtime=200} dofile("HC2.lua") end -- For HC2 emulator
local _version,_fix = "3.0","B90" -- Oct 3, 2020
local _sceneName = "Demo" -- Set to scene/script name
local _homeTable = "devicemap" -- Name of your HomeTable variable (fibaro global)
--local _HueUserName = ".........." -- Hue API key
--local _HueIP = "192.168.1.XX" -- Hue bridge IP
--local _NodeRed = "http://192.168.1.YY:8080/EventRunner" -- Nodered URL
--local _TelegBOT = "t34yt98iughvnw9458gy5of45pg:chr9hcj" -- Telegram BOT key
--local _TelegCID = 6876768686 -- Telegram chat ID
if loadfile then local cr = loadfile("credentials.lua"); if cr then cr() end end
-- To not accidently commit credentials to Github, or post at forum :-)
-- E.g. Hue user names, icloud passwords etc. HC2 credentials is set from HC2.lua, but can use same file.
-- debug flags for various subsystems (global)
_debugFlags = {
post=true,invoke=true,triggers=true,dailys=false,rule=false,ruleTrue=false,
fcall=true, fglobal=false, fget=false, fother=false, hue=true, telegram=false, nodered=false,
}
-- options for various subsystems (global)
_options=_options or {}
-- Hue setup before main() starts. You can add more Hue.connect() inside this if you have more Hue bridges.
--function HueSetup() if _HueUserName and _HueIP then Hue.connect(_HueUserName,_HueIP,"Hue") end end
---------- Main ------------ Here goes your rules ----------------
function main()
local rule,define = Rule.eval, Util.defvar
if _EMULATED then
--_System.speed(true) -- run emulator faster than real-time
_System.setRemote("devices",{17}) -- make device 5 remote (call HC2 with api)
--_System.installProxy() -- Install HC2 proxy sending sourcetriggers back to emulator
end
local HT = -- Example of in-line "home table"
{
dev =
{ bedroom = {lamp = 88, motion = 99},
phones = {bob = 121},
kok = {lamp = 66, motion = 85},
},
other = "other"
}
--or read in "HomeTable" from a fibaro global variable (or scene)
--local HT = type(_homeTable)=='number' and api.get("/scenes/".._homeTable).lua or fibaro:getGlobalValue(_homeTable)
--HT = type(HT) == 'string' and json.decode(HT) or HT
Util.defvars(HT.dev) -- Make HomeTable variables available in EventScript
Util.reverseMapDef(HT.dev) -- Make HomeTable variable names available for logger
vprograms = {
[5] = {{"on","+/00:03"},{"off","+/00:25"}}, -- Can be any number of steps, will repeat from start when reaching end
[8] = {{"on","+/00:03"},{"off","+/00:30"},{"on","+/00:07"},{"off","+/00:18"}},
[1] = {{"on","+/00:10"}}, -- continuously on
[2] = {{"off","+/00:10"}}, -- continuously off
}
rule([[$VarmeEffekt =>
log('Running program %s',$VarmeEffekt);
cancel(timer);
timer = post(#run{step=1,values=vprograms[$VarmeEffekt]})
]])
rule([[#run{step='$step', values='$values'} =>
local v = values[step];
step = step % size(values) + 1;
log('Turning %s',v[1]);
post(#control{action=v[1]});
log('Running next step in %s minutes',v[2]);
timer = post(#run{step=step, values=values},v[2])
]])
rule("#control{action='on'} => kok.lamp:on")
rule("#control{action='off'} => kok.lamp:off")
rule("wait(1); $VarmeEffekt=5")
-- Event.SECTION = 'winter'
-- rule("@@00:00:04 => log('Winter')").disable()
-- Event.SECTION = 'summer'
-- rule("@@00:00:04 => log('Summer')").disable()
-- Event.SECTION = nil
-- rule("wait(2); enable('winter',true); wait(10); enable('summer',true)")
--rule("@@00:00:05 => f=!f; || f >> log('Ding!') || true >> log('Dong!')") -- example rule logging ding/dong every 5 second
--Nodered.connect(_NodeRed) -- Setup nodered functionality
--Telegram.bot(_TelegBOT) -- Setup Telegram BOT that listens on oncoming messages. Only one per BOT.
--Telegram.msg({_TelegCID,_TelegBOT},"Hello") -- Send msg to Telegram without BOT setup
--rule("#Telegram => log(env.event)") -- Receive message from BOT
--rule("@{06:00,catch} => Util.checkVersion()") -- Check for new version every morning at 6:00
--rule("#ER_version => log('New ER version, v:%s, fix:%s',env.event.version,env.event.fix)")
--rule("#ER_version => log('...patching scene'); Util.patchEventRunner()") -- Auto patch new versions...
if _EMULATED then
-- dofile("example_rules3.lua")
end
end
------------------- EventModel - Don't change! --------------------
local function setDefault(GL,V) if _options[GL]==nil then _options[GL]=V end return _options[GL] end
local _RULELOGLENGTH = setDefault('RULELOGLENGTH',80)
local _TIMEADJUST = setDefault('TIMEADJUST',0)
local _STARTONTRIGGER = setDefault('STARTONTRIGGER',false)
local _NUMBEROFBOXES = setDefault('NUMBEROFBOXES',1)
local _MIDNIGHTADJUST = setDefault('MIDNIGHTADJUST',false)
setDefault('DEVICEAUTOACTION',false)
local _VALIDATECHARS = setDefault('VALIDATECHARS',true)
local _NODEREDTIMEOUT = setDefault('NODEREDTIMEOUT',5000)
local _EVENTRUNNERSRCPATH = setDefault('EVENTRUNNERSRCPATH',"EventRunner3.lua")
local _HUETIMEOUT = setDefault('HUETIMEOUT',10000)
local _MARSHALL = setDefault('MARSHALL',true)
setDefault('SUBFILE',nil)
local _MAILBOXES={}
local _MAILBOX = "MAILBOX"..__fibaroSceneId
local _emulator={ids={},adress=nil}
local _supportedEvents = {property=true,global=true,event=true,remote=true}
local _trigger = fibaro:getSourceTrigger()
local _type, _source = _trigger.type, _trigger
function urldecode(str) return str:gsub('%%(%x%x)',function (x) return string.char(tonumber(x,16)) end) end
local function isRemoteEvent(e) return type(e)=='table' and type(e[1])=='string' end -- change in the future...
local function encodeRemoteEvent(e) return {urlencode(json.encode(e)),'%%ER%%'} end
local function decodeRemoteEvent(e) return (json.decode((urldecode(e[1])))) end
local args = fibaro:args()
if _type == 'other' and args and isRemoteEvent(args) then
_trigger,_type = decodeRemoteEvent(args),'remote'
end
---------- Producer(s) - Handing over incoming triggers to consumer --------------------
local _MAXWAIT=5.0 -- seconds to wait
if _supportedEvents[_type] then
local _MBP = _MAILBOX.."_"
local mbp,mb,time,cos,count = 1,nil,os.clock(),nil,fibaro:countScenes()
if not _STARTONTRIGGER then
if count == 1 then fibaro:debug("Aborting: Server not started yet"); fibaro:abort() end
end
if _EMULATED then -- If running in emulated mode, use shortcut to pass event to main instance
local _,env = _System.getInstance(__fibaroSceneId,1) -- if we only could do this on the HC2...
setTimeout(function() env.Event._handleEvent(_trigger) end,nil,"",env)
fibaro:abort()
end
local event = type(_trigger) ~= 'string' and json.encode(_trigger) or _trigger
local ticket = string.format('<@>%s%s',tostring(_source),event)
math.randomseed(time*100000)
cos = math.random(1,_NUMBEROFBOXES)
mbp=cos
repeat
mb = _MBP..mbp
mbp = (mbp % _NUMBEROFBOXES)+1
while(fibaro:getGlobal(mb) ~= "") do
if os.clock()-time>=_MAXWAIT then fibaro:debug("Couldn't post event (dead?), dropping:"..event) fibaro:abort() end
if mbp == cos then fibaro:sleep(10) end
mb = _MBP..mbp
mbp = (mbp % _NUMBEROFBOXES)+1
end
fibaro:setGlobal(mb,ticket) -- try to acquire lock
until fibaro:getGlobal(mb) == ticket -- got lock
fibaro:setGlobal(mb,event) -- write msg
if count>1 then fibaro:abort() end -- and exit
_trigger.type,_type='other','other'
end
---------- Consumer - re-posting incoming triggers as internal events --------------------
do
local _getGlobal,_setGlobal = fibaro.getGlobal, fibaro.setGlobal
function eventConsumer()
local mailboxes,_debugFlags,Event,json = _MAILBOXES,_debugFlags,Event,json
local _CXCS,_CXCST1,_CXCST2=250,os.clock()
local function poll()
_CXCS = math.min(2*(_CXCS+1),250)
_CXCST1,_CXCST2 = os.clock(),_CXCST1
if _CXCST1-_CXCST2 > 0.75 then Log(LOG.ERROR,"Slow mailbox watch:%ss",_CXCST1-_CXCST2) end
for _,mb in ipairs(mailboxes) do
local l = _getGlobal(nil,mb)
if l and l ~= "" and l:sub(1,3) ~= '<@>' then -- Something in the mailbox
_setGlobal(nil,mb,"") -- clear mailbox
if _debugFlags.triggers then Debug(true,"Incoming event:"..l) end
l = json.decode(l)
if type(l)=='table' then
l._sh=true setTimeout(function() Event.triggerHandler(l) end,5) -- post to "main()"
else
Debug(true,"Bad incoming event:"..l)
end
_CXCS=1
end
end
setTimeout(poll,_CXCS) -- check again
end
poll()
end
end
---------- Event manager --------------------------------------
function makeEventManager()
local self,_handlers = {},{}
self.BREAK, self.TIMER, self.RULE ='%%BREAK%%', '%%TIMER%%', '%%RULE%%'
self.PING, self.PONG ='%%PING%%', '%%PONG%%'
self._sections,self.SECTION = {},nil
local isTimer,isEvent,isRule,coerce,format,toTime = Util.isTimer,Util.isEvent,Util.isRule,Util.coerce,string.format,Util.toTime
local equal,copy = Util.equal,Util.copy
local function timer2str(t)
return format("<timer:%s, start:%s, stop:%s>",t[self.TIMER],os.date("%c",t.start),os.date("%c",math.floor(t.start+t.len/1000+0.5)))
end
local function mkTimer(f,t) t=t or 0; return {[self.TIMER]=setTimeout(f,t), start=os.time(), len=t, __tostring=timer2str} end
local constraints = {}
constraints['=='] = function(val) return function(x) x,val=coerce(x,val) return x == val end end
constraints['>='] = function(val) return function(x) x,val=coerce(x,val) return x >= val end end
constraints['<='] = function(val) return function(x) x,val=coerce(x,val) return x <= val end end
constraints['>'] = function(val) return function(x) x,val=coerce(x,val) return x > val end end
constraints['<'] = function(val) return function(x) x,val=coerce(x,val) return x < val end end
constraints['~='] = function(val) return function(x) x,val=coerce(x,val) return x ~= val end end
constraints[''] = function(_) return function(x) return x ~= nil end end
local function compilePattern2(pattern)
if type(pattern) == 'table' then
if pattern._var_ then return end
for k,v in pairs(pattern) do
if type(v) == 'string' and v:sub(1,1) == '$' then
local var,op,val = v:match("$([%w_]*)([<>=~]*)([+-]?%d*%.?%d*)")
var = var =="" and "_" or var
local c = constraints[op](tonumber(val))
pattern[k] = {_var_=var, _constr=c, _str=v}
else compilePattern2(v) end
end
end
end
local function compilePattern(pattern)
compilePattern2(pattern)
if pattern.type and type(pattern.deviceID)=='table' and not pattern.deviceID._constr then
local m = {}; for _,id in ipairs(pattern.deviceID) do m[id]=true end
pattern.deviceID = {_var_='_', _constr=function(val) return m[val] end, _str=pattern.deviceID}
end
end
self._compilePattern = compilePattern
function self._match(pattern, expr)
local matches = {}
local function _unify(pattern,expr)
if pattern == expr then return true
elseif type(pattern) == 'table' then
if pattern._var_ then
local var, constr = pattern._var_, pattern._constr
if var == '_' then return constr(expr)
elseif matches[var] then return constr(expr) and _unify(matches[var],expr) -- Hmm, equal?
else matches[var] = expr return constr(expr) end
end
if type(expr) ~= "table" then return false end
for k,v in pairs(pattern) do if not _unify(v,expr[k]) then return false end end
return true
else return false end
end
return _unify(pattern,expr) and matches or false
end
function self._callTimerFun(e,src)
local status,res,ctx = spcall(e)
if not status then
if not isError(res) then
res={ERR=true,ctx=ctx,src=src or tostring(e),err=res}
end
Log(LOG.ERROR,"Error in '%s': %s",res and res.src or tostring(e),res.err)
if res.ctx then Log(LOG.ERROR,"\n%s",res.ctx) end
end
end
function self.post(e,time,src) -- time in 'toTime' format, see below.
_assert(isEvent(e) or type(e) == 'function', "Bad2 event format %s",tojson(e))
time = toTime(time or os.time())
if time < os.time() then return nil end
if type(e) == 'function' then
src = src or "timer "..tostring(e)
if _debugFlags.postTimers then Debug(true,"Posting timer %s at %s",src,os.date("%a %b %d %X",time)) end
return mkTimer(function() self._callTimerFun(e,src) end, 1000*(time-os.time()))
end
src = src or tojson(e)
if _debugFlags.post and not e._sh then Debug(true,"Posting %s at %s",tojson(e),os.date("%a %b %d %X",time)) end
return mkTimer(function() self._handleEvent(e) end,1000*(time-os.time()))
end
function self.cancel(t)
_assert(isTimer(t) or t == nil,"Bad timer")
if t then clearTimeout(t[self.TIMER]) end
return nil
end
self.triggerHandler = self.post -- default handler for consumer
function self.postRemote(sceneID, e) -- Post event to other scenes
_assert(sceneID and tonumber(sceneID),"sceneID is not a number to postRemote:%s",sceneID or "");
_assert(isEvent(e),"Bad event format to postRemote")
e._from = _EMULATED and -__fibaroSceneId or __fibaroSceneId
local payload = encodeRemoteEvent(e)
if not _EMULATED then -- On HC2
if sceneID < 0 then -- call emulator
if not _emulator.adress then return end
local HTTP = net.HTTPClient()
HTTP:request(_emulator.adress.."trigger/"..sceneID,{options = {
headers = {['Accept']='application/json',['Content-Type']='application/json'},
data = json.encode(payload), timeout=2000, method = 'POST'},
-- Can't figure out why we get an and of file - must depend on HC2.lua
error = function(status) if status~="End of file" then Log(LOG.ERROR,"Emulator error:%s, (%s)",status,tojson(e)) end end,
success = function(status) end,
})
else
fibaro:startScene(sceneID,payload)
end -- call other scene on HC2
else -- on emulator
fibaro:startScene(math.abs(sceneID),payload)
end
end
local _getIdProp = function(id,prop) return fibaro:getValue(id,prop) end
local _getGlobal = function(id) return fibaro:getGlobalValue(id) end
local _getProp = {}
_getProp['property'] = function(e,v)
e.propertyName = e.propertyName or 'value'
e.value = v or (_getIdProp(e.deviceID,e.propertyName,true))
self.trackManual(e.deviceID,e.value)
return nil -- was t
end
_getProp['global'] = function(e,v2) local v,t = _getGlobal(e.name,true) e.value = v2 or v return t end
local function ruleToStr(r) return r.src end
function self._mkCombEvent(e,action,doc,rl)
local rm = {[self.RULE]=e, action=action, src=doc, cache={}, subs=rl}
rm.enable = function() Util.mapF(function(e) e.enable() end,rl) return rm end
rm.disable = function() Util.mapF(function(e) e.disable() end,rl) return rm end
rm.start = function(event) self._invokeRule({rule=rm,event=event}) return rm end
rm.print = function() Util.map(function(e) e.print() end,rl) end
rm.__tostring = ruleToStr
return rm
end
local toHash,fromHash={},{}
fromHash['property'] = function(e) return {e.type..e.deviceID,e.type} end
fromHash['global'] = function(e) return {e.type..e.name,e.type} end
toHash['property'] = function(e) return e.deviceID and 'property'..e.deviceID or 'property' end
toHash['global'] = function(e) return e.name and 'global'..e.name or 'global' end
local function handlerEnable(t,handle)
if type(handle) == 'string' then Util.mapF(self[t],Event._sections[handle] or {})
elseif isRule(handle) then handle[t]()
elseif type(handle) == 'table' then Util.mapF(self[t],handle)
else error('Not an event handler') end
return true
end
function self.enable(handle,opt)
if type(handle)=='string' and opt then
for s,e in pairs(self._sections or {}) do
if s ~= handle then handlerEnable('disable',e) end
end
end
return handlerEnable('enable',handle)
end
function self.disable(handle) return handlerEnable('disable',handle) end
function self.event(e,action,opt) -- define rules - event template + action
opt=opt or {}
local doc,front = opt.doc or nil, opt.front
doc = doc or format(" Event.event(%s,...)",tojson(e))
if e[1] then -- events is list of event patterns {{type='x', ..},{type='y', ...}, ...}
return self._mkCombEvent(e,action,doc,Util.map(function(es) return self.event(es,action,opt) end,e))
end
_assert(isEvent(e), "bad event format '%s'",tojson(e))
if e.deviceID and type(e.deviceID) == 'table' then -- multiple IDs in deviceID {type='property', deviceID={x,y,..}}
return self.event(Util.map(function(id) local el=copy(e); el.deviceID=id return el end,e.deviceID),action,opt)
end
action = self._compileAction(action,doc,opt.log)
compilePattern(e)
local hashKey = toHash[e.type] and toHash[e.type](e) or e.type
_handlers[hashKey] = _handlers[hashKey] or {}
local rules = _handlers[hashKey]
local rule,fn = {[self.RULE]=e, action=action, src=doc, log=opt.log, cache={}}, true
for _,rs in ipairs(rules) do -- Collect handlers with identical patterns. {{e1,e2,e3},{e1,e2,e3}}
if equal(e,rs[1][self.RULE]) then if front then table.insert(rs,1,rule) else rs[#rs+1] = rule end fn = false break end
end
if fn then if front then table.insert(rules,1,{rule}) else rules[#rules+1] = {rule} end end
rule.enable = function() rule._disabled = nil return rule end
rule.disable = function() rule._disabled = true return rule end
rule.start = function(event) self._invokeRule({rule=rule,event=event}) return rule end
rule.print = function() Log(LOG.LOG,"Event(%s) => ..",tojson(e)) end
rule.__tostring = ruleToStr
if self.SECTION then
local s = self._sections[self.SECTION] or {}
s[#s+1] = rule
self._sections[self.SECTION] = s
end
return rule
end
function self.schedule(time,action,opt)
opt = opt or {}
local test,start,doc = opt.cond, opt.start or false, opt.doc or format("Schedule(%s):%s",time,tostring(action))
local loop,tp = {type='_scheduler:'..doc, _sh=true}
local test2,action2 = test and self._compileAction(test,doc,opt.log),self._compileAction(action,doc,opt.log)
local re = self.event(loop,function(env)
local fl = test == nil or test2()
if fl == self.BREAK then return
elseif fl then action2() end
tp = self.post(loop, time, doc)
end)
local res = nil
res = {
[self.RULE] = {}, src=doc,
enable = function() if not tp then tp = self.post(loop,(not start) and time or nil,doc) end return res end,
disable= function() tp = self.cancel(tp) return res end,
print = re.print,
__tostring = ruleToStr
}
res.enable()
return res
end
local _trueFor={ property={'value'}, global = {'value'}}
function self.trueFor(time,event,action,name)
local pattern,ev,ref = copy(event),copy(event),nil
name=name or tojson(event)
compilePattern(pattern)
if _trueFor[ev.type] then
for _,p in ipairs(_trueFor[ev.type]) do ev[p]=nil end
else error(format("trueFor can't handle events of type '%s'%s",event.type,name)) end
return Event.event(ev,function(env)
local p = self._match(pattern,env.event)
if p then env.p = p; self.post(function() ref=nil action(env) end, time, name) else self.cancel(ref) end
end)
end
function self.pollTriggers(devices)
local filter = {}
local function truthTable(t) local res={}; for _,p in ipairs(t) do res[p]=true end return res end
for id,t in pairs(devices) do filter[id]=truthTable(type(t)=='table' and t or {t}) end
local INTERVAL = 2
local lastRefresh = 0
local function pollRefresh()
states = api.get("/refreshStates?last=" .. lastRefresh)
if states then
lastRefresh=states.last
for k,v in pairs(states.changes or {}) do
for p,a in pairs(v) do
if p~='id' and filter[v.id] and filter[v.id][p] then
local e = {type='property', deviceID=v.id,propertyName=p, value=a}
print(json.encode(e))
end
end
end
end
setTimeout(pollRefresh,INTERVAL*1000)
end
pollRefresh()
end
function self._compileAction(a,src,log)
if type(a) == 'function' then return a -- Lua function
elseif isEvent(a) then
return function(e) return self.post(a,nil,e.rule) end -- Event -> post(event)
elseif type(a)=='string' or type(a)=='table' then -- EventScript
src = src or a
local code = type(a)=='string' and ScriptCompiler.compile(src,log) or a
local function run(env)
env=env or {}; env.log = env.log or {}; env.log.cont=env.log.cont or function(...) return ... end
env.locals = env.locals or {}
local co = coroutine.create(code,src,env); env.co = co
local res={coroutine.resume(co)}
if res[1]==true then
if coroutine.status(co)=='dead' then return env.log.cont(select(2,table.unpack(res))) end
else error(res[1]) end
end
return run
end
error("Unable to compile action:"..json.encode(a))
end
function self._invokeRule(env,event)
local t = os.time()
env.last,env.rule.time,env.log = t-(env.rule.time or 0),t,env.rule.log
env.event = env.event or event
if _debugFlags.invoke and (env.event == nil or not env.event._sh) then Debug(true,"Invoking:%s",env.rule.src) end
local status, res, ctx = spcall(env.rule.action,env) -- call the associated action
if not status then
if not isError(res) then
res={ERR=true,ctx=ctx,src=env.src,err=res}
end
Log(LOG.ERROR,"Error in '%s': %s",res and res.src or "rule",res.err)
if res.ctx then Log(LOG.ERROR,"\n%s",res.ctx) end
self.post({type='error',err=res,rule=res.src,event=tojson(env.event),_sh=true}) -- Send error back
env.rule._disabled = true -- disable rule to not generate more errors
end
end
-- {{e1,e2,e3},{e4,e5,e6}} env={event=_,p=_,locals=_,rule.src=_,last=_}
function self._handleEvent(e) -- running a posted event
if _getProp[e.type] then _getProp[e.type](e,e.value) end -- patch events
local _match,hasKeys = self._match,fromHash[e.type] and fromHash[e.type](e) or {e.type}
for _,hashKey in ipairs(hasKeys) do
for _,rules in ipairs(_handlers[hashKey] or {}) do -- Check all rules of 'type'
local match,m = _match(rules[1][self.RULE],e),nil
if match then
for _,rule in ipairs(rules) do
if not rule._disabled then
m={}; if next(match) then for k,v in pairs(match) do m[k]={v} end end
local env = {event = e, p=match, rule=rule, locals= m}
self._invokeRule(env)
end
end
end
end
end
end
-- Extended fibaro:* commands, toggle, setValue, User defined device IDs, > 10000
fibaro._idMap={}
fibaro._call,fibaro._get,fibaro._getValue,fibaro._actions,fibaro._properties=fibaro.call,fibaro.get,fibaro.getValue,{},{}
local lastID = {}
fibaro._valueTriggers={} -- setup trigger table
local devs = api.get("/scenes/"..__fibaroSceneId)
devs = devs and devs.triggers; devs = devs and devs.properties or {}
for _,d in ipairs(devs) do if d.name=='value' then fibaro._valueTriggers[d.id] = true end end
function self.lastManual(id)
id=tonumber(id); lastID[id] = lastID[id] or {time=0}
return lastID[id].script=='seen' and -1 or os.time()-lastID[id].time
end
function self.trackManual(id,value)
id=tonumber(id); lastID[id] = lastID[id] or {time=0}
if lastID[id].script==true and fibaro._valueTriggers[id] then -- triggered by script
lastID[id].script='seen'
else lastID[id]={time=os.time()} end
end
function self._registerID(id,call,get) fibaro._idMap[id]={call=call,get=get} end
-- We intercept fibaro:call, fibaro:get, and fibaro:getValue - we may change this to an object model
local _DEFACTIONS={wakeUpDeadDevice=true, setProperty=true}
local patchHook = {}
function patchHook.toggle(obj,id,call,...) fibaro.call(obj,id,fibaro:getValue(id,"value")>"0" and "turnOff" or "turnOn") return true end
function patchHook.dim(obj,id,call,...) Util.dimLight(id,...) return true end
function patchHook.turnOn(obj,id,call,...) if fibaro._checkOp and fibaro:getValue(id,"value")>'0' then return true end end
function patchHook.turnOff(obj,id,call,...) if fibaro._checkOp and fibaro:getValue(id,"value")=='0' then return true end end
function patchHook.setValue(obj,id,call,val) if fibaro._checkOp and fibaro:getValue(id,"value")==val then return true end end
function fibaro.call(obj,id,call,...)
local args = (({...})[1])
id = tonumber(id); if not id then error("deviceID not a number",2) end
if ({turnOff=true,turnOn=true,on=true,off=true,setValue=true})[call] then lastID[id]={script=true,time=os.time()} end
if patchHook[call] and patchHook[call](obj,id,call,...) then return end
if fibaro._idMap[id] then return fibaro._idMap[id].call(obj,id,call,...) end
-- Now we have a real deviceID
if select(2,__fibaro_get_device(id)) == 404 then Log(LOG.ERROR,"No such deviceID:%s",id) return end
fibaro._actions[id] = fibaro._actions[id] or api.get("/devices/"..id).actions
if call=='setValue' and not fibaro._actions[id].setValue and fibaro._actions[id].turnOn then
return fibaro._call(obj,id,tonumber(({...})[1]) > 0 and "turnOn" or "turnOff")
end
if _options.DEVICEAUTOACTION or _DEFACTIONS[call] then fibaro._actions[id][call]="1" end
_assert(fibaro._actions[id][call],"ID:%d does not support action '%s'",id,call)
return fibaro._call(obj,id,call,...)
end
local _DEV_PROP_MAP={["IPAddress"]='ip', ["TCPPort"]='port'}
function fibaro.get(obj,id,prop,...)
id = tonumber(id); if not id then error("deviceID not a number",2) end
if fibaro._idMap[id] then return fibaro._idMap[id].get(obj,id,prop,...) end
if select(2,__fibaro_get_device(id)) == 404 then Log(LOG.ERROR,"No such deviceID:%s",id) return end
fibaro._properties[id] = fibaro._properties[id] or api.get("/devices/"..id).properties
if not _DEV_PROP_MAP[prop] then
_assert(fibaro._properties[id][prop]~=nil,"ID:%d does not support property '%s'",id,prop)
end
return fibaro._get(obj,id,prop,...)
end
function fibaro.getValue(obj,id,prop,...)
id = tonumber(id); if not id then error("deviceID not a number",2) end
if fibaro._idMap[id] then return (fibaro._idMap[id].get(obj,id,prop,...)) end
if select(2,__fibaro_get_device(id)) == 404 then Log(LOG.ERROR,"No such deviceID:%s",id) return end
fibaro._properties[id] = fibaro._properties[id] or api.get("/devices/"..id).properties
_assert(fibaro._properties[id][prop]~=nil,"ID:%d does not support property '%s'",id,prop)
return fibaro._getValue(obj,id,prop,...)
end
-- Logging of fibaro:* calls -------------
local function traceFibaro(name,flag,rt)
local orgFun=fibaro[name]
fibaro[name]=function(f,id,...)
local args={...}
local stat,res = pcall(function() return {orgFun(f,id,table.unpack(args))} end)
if stat then
if _debugFlags[flag] then
if rt then rt(id,args,res)
else
local astr=(id~=nil and Util.reverseVar(id).."," or "")..json.encode(args):sub(2,-2)
Debug(true,"fibaro:%s(%s)%s",name,astr,#res>0 and "="..tojson(res):sub(2,-2) or "")
end
end
if #res>0 then return table.unpack(res) else return nil end
else
local astr=(id~=nil and Util.reverseVar(id).."," or "")..json.encode(args):sub(2,-2)
error(format("fibaro:%s(%s),%s",name,astr,res),3)
end
end
end
if not _EMULATED then -- Emulator logs fibaro:* calls for us
local maps = {
{"call","fcall"},{"setGlobal","fglobal"},{"getGlobal","fglobal"},{"getGlobalValue","fglobal"},
{"get","fget"},{"getValue","fget"},{"killScenes","fother"},{"abort","fother"},
{"sleep","fother",function(id,args,res)
Debug(true,"fibaro:sleep(%s) until %s",id,os.date("%X",os.time()+math.floor(0.5+id/1000)))
end},
{"startScene","fother",function(id,args,res)
local a = isRemoteEvent(args[1]) and json.encode(decodeRemoteEvent(args[1])) or args and json.encode(args)
Debug(true,"fibaro:startScene(%s%s)",id,a and ","..a or "")
end},
}
for _,f in ipairs(maps) do traceFibaro(f[1],f[2],f[3]) end
end
function fibaro:sleep() error("Not allowed to use fibaro:sleep in EventRunner scenes!") end
return self
end
---------- Utilities --------------------------------------
local function makeUtils()
local LOG = {WELCOME = "orange",DEBUG = "white", SYSTEM = "Cyan", LOG = "green", ULOG="Khaki", ERROR = "Tomato"}
local self,format = {},string.format
gEventRunnerKey="6w8562395ue734r437fg3"
gEventSupervisorKey="9t823239".."5ue734r327fh3"
if not _EMULATED then -- Patch possibly buggy setTimeout - what is 1ms between friends...
clearTimeout,oldClearTimout=function(ref)
if type(ref)=='table' and ref[1]=='%EXT%' then ref=ref[2] end
oldClearTimout(ref)
end,clearTimeout
setTimeout,oldSetTimout=function(f,ms)
local ref,maxt={'%EXT%'},2147483648-1
ms = ms and ms < 1 and 1 or ms
if ms > maxt then
ref[2]=oldSetTimout(function() ref[2 ]=setTimeout(f,ms-maxt)[2] end,maxt)
else ref[2 ]=oldSetTimout(f,ms) end
return ref
end,setTimeout
end
local function prconv(o)
if type(o)=='table' then
if o.__tostring then return o.__tostring(o)
else return tojson(o) end
else return o end
end
local function prconvTab(args) local r={}; for _,o in ipairs(args) do r[#r+1]=prconv(o) end return r end
local function test(color,msg,...)
local t = type(...)
y = t
end
local function _Msg(color,message,...)
local args = type(... or 42) == 'function' and {(...)()} or {...}
local tadj = _TIMEADJUST > 0 and os.date("(%X) ") or ""
message = #args > 0 and format(message,table.unpack(prconvTab(args))) or prconv(message)
fibaro:debug(format('<span style="color:%s;">%s%s</span><br>', color, tadj, message))
return message
end
if _System and _System._Msg then _Msg=_System._Msg end -- Get a better ZBS version of _Msg if running emulated
local function protectMsg(...)
local args = {...}
local stat,res=pcall(function() return _Msg(table.unpack(args)) end)
if not stat then error("Bad arguments to Log/Debug:"..tojson(args),2)
else return res 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
function Debug(flag,message,...) if flag then _Msg(LOG.DEBUG,message,...) end end
function Log(color,message,...) return protectMsg(color,message,...) end
function isError(e) return type(e)=='table' and e.ERR end
function throwError(args) args.ERR=true; error(args,args.level) end
function spcall(fun,...)
local stat={pcall(fun,...)}
if not stat[1] then
local msg = type(stat[2])=='table' and stat[2].err or stat[2]
local line,l,src,lua = tonumber(msg:match(":(%d+)")),0,{},nil
if line == nil or line == "" then return false,stat[2],"" end
if _options.SUBFILE and msg:match(_options.SUBFILE..":"..line) then
local f = io.open(_options.SUBFILE); lua = f:read("*all")
else lua = api.get("/scenes/"..__fibaroSceneId).lua end
for row in lua:gmatch(".-\n") do l=l+1;
if math.abs(line-l)<3 then src[#src+1]=string.format("Line %d:%s%s",l,(l==line and ">>>" or ""),row) end
end
return false,stat[2],table.concat(src)
end
return table.unpack(stat)
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 self.gensym(s) return s..tostring({1,2,3}):match("([abcdef%d]*)$") end
local function transform(obj,tf)
if type(obj) == 'table' then
local res = {} for l,v in pairs(obj) do res[l] = Util.transform(v,tf) end
return res
else return tf(obj) end
end
function self.copy(obj) return transform(obj, function(o) return o end) end
local function equal(e1,e2)
local t1,t2 = type(e1),type(e2)
if t1 ~= t2 then return false end
if t1 ~= 'table' and t2 ~= 'table' then return e1 == e2 end
for k1,v1 in pairs(e1) do if e2[k1] == nil or not equal(v1,e2[k1]) then return false end end
for k2,v2 in pairs(e2) do if e1[k2] == nil or not equal(e1[k2],v2) then return false end end
return true
end
function self.randomizeList(list)
local res,l,j,n = {},{}; for _,v in pairs(list) do l[#l+1]=v end
n=#l
for i=n,1,-1 do j=math.random(1,i); res[#res+1]=l[j]; table.remove(l,j) end
return res
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
local function time2str(t) return format("%02d:%02d:%02d",math.floor(t/3600),math.floor((t%3600)/60),t%60) end
local function midnight() local t = os.date("*t"); t.hour,t.min,t.sec = 0,0,0; return os.time(t) end
local function hm2sec(hmstr)
local offs,sun
sun,offs = hmstr:match("^(%a+)([+-]?%d*)")
if sun and (sun == 'sunset' or sun == 'sunrise') then
hmstr,offs = fibaro:getValue(1,sun.."Hour"), tonumber(offs) or 0
end
local sg,h,m,s = hmstr:match("^(%-?)(%d+):(%d+):?(%d*)")
_assert(h and m,"Bad hm2sec string %s",hmstr)
return (sg == '-' and -1 or 1)*(h*3600+m*60+(tonumber(s) or 0)+(offs or 0)*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
local function toDate(str)
local y,m,d = str:match("(%d%d%d%d)/(%d%d)/(%d%d)")
return os.time{year=tonumber(y),month=tonumber(m),day=tonumber(d),hour=0,min=0,sec=0}
end
-- toTime("10:00") -> 10*3600+0*60 secs
-- toTime("10:00:05") -> 10*3600+0*60+5*1 secs
-- toTime("t/10:00") -> (t)oday at 10:00. midnight+10*3600+0*60 secs
-- toTime("n/10:00") -> (n)ext time. today at 10.00AM if called before (or at) 10.00AM else 10:00AM next day
-- toTime("+/10:00") -> Plus time. os.time() + 10 hours
-- toTime("+/00:01:22") -> Plus time. os.time() + 1min and 22sec
-- toTime("sunset") -> todays sunset in relative secs since midnight, E.g. sunset="05:10", =>toTime("05:10")
-- toTime("sunrise") -> todays sunrise
-- toTime("sunset+10") -> todays sunset + 10min. E.g. sunset="05:10", =>toTime("05:10")+10*60
-- toTime("sunrise-5") -> todays sunrise - 5min
-- toTime("t/sunset+10")-> (t)oday at sunset in 'absolute' time. E.g. midnight+toTime("sunset+10")
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
self.toTime,self.midnight,self.toDate,self.time2str,self.transform,self.equal=toTime,midnight,toDate,time2str,transform,equal
function self.isTimer(t) return type(t) == 'table' and t[Event.TIMER] end
function self.isRule(r) return type(r) == 'table' and r[Event.RULE] end
function self.isEvent(e) return type(e) == 'table' and e.type end
function self.isTEvent(e) return type(e)=='table' and (e[1]=='%table' or e[1]=='%quote') and type(e[2])=='table' and e[2].type end
function self.coerce(x,y) local x1 = tonumber(x) if x1 then return x1,tonumber(y) else return x,y end 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"}
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
function self.validateChars(str,msg)
if _VALIDATECHARS then local p = str:find("\xEF\xBB\xBF") if p then error(format("Char:%s, "..msg,p,str)) end end
end
function self.fixMLesc(str) return (str:gsub("(\\%d%d%d)",function(w) return string.char(tonumber(w:sub(2))) end)) end
local gKeys = {type=1,deviceID=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
function self.printRule(rule)
Log(LOG.LOG,"-----------------------------------")
Log(LOG.LOG,"Source:'%s'",rule.src)
rule.print()
Log(LOG.LOG,"-----------------------------------")
end
function self.dump(code)
code = code or {}
for p = 1,#code do
local i = code[p]
Log(LOG.LOG,"%-3d:[%s/%s%s%s]",p,i[1],i[2] ,i[3] and ","..tojson(i[3]) or "",i[4] and ","..tojson(i[4]) or "")
end
end
self.getIDfromEvent={ CentralSceneEvent=function(d) return d.deviceId end,AccessControlEvent=function(d) return d.id end }
self.getIDfromTrigger={
property=function(e) return e.deviceID end,
event=function(e) return e.event and Util.getIDfromEvent[e.event.type or ""](e.event.data) 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,res = ScriptEngine.eval(co.context)
co.state= status=='suspended' and status or 'dead'
return true,table.unpack(res)
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
}
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]={[(h*60+m+offs)%60]=true}
dateSeq[2]={[math.floor((h*60+m+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
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