-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathI_Submasters1.xml
493 lines (447 loc) · 17.4 KB
/
I_Submasters1.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<!-- I_Submasters1.xml -->
<implementation>
<functions>
--[[ ------------------------------------------------------------------
Submasters
------------------------------------------------------------------ --]]
local PLUGIN_MODULE_NAME = "L_Submasters1" -- !!! Modify this to match the Lua main module name for the plugin (no .lua suffix here!)
--[[ ===== D O N O T M O D I F Y B E L O W T H I S L I N E --]]
local pluginModule
local logLevels = { ERR=1, ERROR=1, WARN=2, WARNING=2, NOTICE=3, INFO=4, DEBUG1=5, DEBUG2=6,
err=1, error=1, warn=2, warning=2, notice=3, info=4, debug1=5, debug2=6,
DEFAULT=4, default=4,
[1]='err', [2]='warn', [3]='notice', [4]='info', [5]='debug1', [6]='debug2' }
-- Used for output of tables to debug stream
local function dump(t, seen)
if t == nil then return "nil" end
seen = seen or {}
local sep = ""
local str = "{ "
for k,v in pairs(t) do
local val
if type(v) == "table" then
if seen[v] then val = "(recursion)"
else
seen[v] = true
val = dump(v, seen)
end
elseif type(v) == "string" then
val = string.format("%q", v)
else
val = tostring(v)
end
str = str .. sep .. k .. "=" .. val
sep = ", "
end
str = str .. " }"
return str
end
-- Write a message to the log
function L(level, msg, ...)
if type(level) == "string" then level = logLevels[level] or logLevels.notice end
if (level or logLevels.notice) <= (PFB.loglevel or logLevels.DEFAULT) then
local str
local ll = ( level == logLevels.err ) and 1 or ( ( level == logLevels.warn ) and 2 or 50 )
if type(msg) == "table" then
str = tostring(msg.prefix or pluginName) .. ": " .. tostring(msg.msg)
ll = msg.level or ll
else
str = string.format( "[%s] %s: %s", logLevels[level] or '?', tostring(pluginName), tostring(msg) )
end
str = string.gsub(str, "%%(%d+)", function( n )
n = tonumber(n, 10)
if n < 1 or n > #arg then return "nil" end
local val = arg[n]
if type(val) == "table" then
return dump(val)
elseif type(val) == "string" then
return string.format("%q", val)
elseif type(val) == "number" and math.abs(val-os.time()) <= 86400 then
return tostring(val) .. "(" .. os.date("%x.%X", val) .. ")"
end
return tostring(val)
end
)
luup.log(str, ll)
end
end
local function P(msg, ...) if pluginFlags.debug then L('notice', {msg=msg,prefix="[Plugin".."Bas".."ic]"},...) end end
-- Get variable value (easy to go directly to luup, here just for consistency)
local function getVar( var, dev, sid )
return luup.variable_get( sid or pluginModule.MYSID, var, dev or PFB.device )
end
-- Get numeric variable, or return default value if unset/empty/non-numeric
local function getVarNumeric( name, dflt, dev, sid )
sid = sid or pluginModule.MYSID -- use MYSID if sid not passed
dev = dev or PFB.device -- use PFB.device if dev not passed
local s = luup.variable_get( sid, name, dev ) or ""
if s == "" then return dflt end
return tonumber(s) or dflt
end
-- Initialize a variable if it does not already exist.
local function initVar( name, dflt, dev, sid )
sid = sid or pluginModule.MYSID -- use MYSID if sid not passed
dev = dev or PFB.device -- use PFB.device if dev not passed
local currVal = luup.variable_get( sid, name, dev )
if currVal == nil then
luup.variable_set( sid, name, tostring(dflt), dev )
return tostring(dflt)
end
return currVal
end
-- Set variable, only if value has changed.
local function setVar( name, val, dev, sid )
sid = sid or pluginModule.MYSID -- use MYSID if sid not passed
dev = dev or PFB.device -- use PFB.device if dev not passed
val = (val == nil) and "" or tostring(val)
local s = luup.variable_get( sid, name, dev )
if s ~= val then
luup.variable_set( sid, name, val, dev )
end
return s -- return old value
end
-- Delete a state variable. Newer versions of firmware do this by setting nil;
-- older versions require a request (not implemented here).
local function deleteVar( name, dev, sid )
sid = sid or pluginModule.MYSID -- use MYSID if sid not passed
dev = dev or PFB.device -- use PFB.device if dev not passed
if luup.variable_get( sid, name, dev ) then
luup.variable_set( sid, name, nil, dev )
end
end
-- Find next timer to fire
local function findNextTimer()
local mintimer
for _,t in pairs( pluginTimers ) do
if t.id ~= "master" and ( mintimer == nil or t.when < mintimer.when ) then mintimer = t end
end
P("findNextTimer() next is %1", mintimer)
return mintimer
end
-- (Re)schedule next master tick
local function scheduleNextDelayRun()
P("scheduleNextDelayRun()")
local mintimer = findNextTimer()
if mintimer then
if pluginTimers.master and ( pluginTimers.master.when == 0 or mintimer.when >= pluginTimers.master.when ) then
-- Master not waiting (execTimers is running) or next eligible later than current
-- master tick, don't reschedule.
P("scheduleNextDelayRun() in exec or new timer past master, not rescheduling")
return
end
local delay = math.max( mintimer.when - os.time(), 0 )
pluginMasterSerial = pluginMasterSerial + 1
pluginTimers.master = { id="master", when=os.time()+delay, serial=pluginMasterSerial }
P("scheduleNextDelayRun() master tick now serial %1 scheduled for %2; master=%3", pluginMasterSerial, delay, master)
luup.call_delay( '_DelayCb', delay, tostring( pluginMasterSerial) )
end
P("All current timers: %1", pluginTimers)
end
-- Schedule a function for delayed execution. Creates a new timer.
local function pluginTimer( seconds, func, ... )
P("pluginTimer(%1,%2,...)", seconds, func)
local pid
if type( seconds ) == "table" then
pid = tostring( seconds.id )
seconds = tonumber( seconds.seconds ) or error "Invalid delay (non-numeric: "..tostring(seconds.seconds)..")"
else
seconds = tonumber( seconds ) or error "Invalid delay (non-numeric: "..tostring(seconds)..")"
end
if not pid or pid == "" then
pid = string.format("d-%x", pluginNextTID )
pluginNextTID = pluginNextTID + 1
end
if seconds < 0 then seconds = 0 end
pluginTimers[pid] = { id=pid, when=os.time()+seconds, func=func, args=arg }
scheduleNextDelayRun()
return pid
end
-- Set up an interval on a recurring timer.
local function pluginInterval( seconds, func, ... )
P("pluginInterval(%1,%2,...)", seconds, func)
local pid = pluginTimer( seconds, func, ... )
pluginTimers[pid].interval = seconds
return pid
end
local function nulltimer() L('warn', "Timer task has no callback function") end
-- Run eligible timers
local function execTimers( lcbparm )
P("execTimers(%1)", lcbparm)
pluginTimers.master.when = 0 -- flag not waiting
local run = {}
local now = os.time()
for _,v in pairs( pluginTimers ) do
if v.id ~= "master" and v.when and v.when <= now then
table.insert( run, v.id )
end
end
-- Sort by time
table.sort( run, function( a, b ) return pluginTimers[a].when < pluginTimers[b].when end )
P("execTimers() found %1 eligible timers to run: %2", #run, run)
for _,id in ipairs( run ) do
local v = pluginTimers[id]
local st = os.time()
P("execTimers() running timer %1 due %2 late %3", v.id, v.when, os.time()-v.when)
local success, err = pcall( v.func or nulltimer, unpack( v.args or {} ) )
if not success then L('err', "Timer task %1 failed: %2", v.id, err) end
local rt = os.time() - st
if not v.interval then
pluginTimers[v.id] = nil
else
if rt >= v.interval then L('warn', "Interval task %1 took longer to execute (%2s) than interval (%3s)", v.id, rt, v.interval) end
while v.when <= now do v.when = v.when + v.interval end -- improve: use maths
end
end
local nextTimer = findNextTimer()
if not nextTimer then
-- Nothing more to run. Remove master tick.
pluginTimers.master = nil
return
end
-- Schedule next delay for next task.
local delay = math.max( nextTimer.when - os.time(), 0 )
pluginTimers.master.when = os.time() + delay
luup.call_delay( '_DelayCb', delay, lcbparm )
end
local function cancelTimer( delayId )
pluginTimers[tostring(delayId)] = nil
end
-- Place watch on a device state variable
local function pluginDeviceWatch( dev, sid, var, func, ... )
P("pluginWatch(%1,%2,%3)", dev, sid, var)
local key = string.format( "%s/%s/%s", tostring(dev), tostring(sid), tostring(var))
P("key is %1", key)
if not pluginWatches[key] then
P("Watching %1", key)
luup.variable_watch( "_WatchCb", sid, var, dev )
pluginWatches[key] = {}
end
local fkey = string.format( "%d:%s", PFB.device, tostring(func) )
P("subscribing %1 to %2", fkey, key)
pluginWatches[key][fkey] = { id=key, dev=PFB.device, func=func, args=arg }
end
-- Remove watch on device state variable
local function pluginDeviceWatchClear( dev, sid, var, func )
local key = string.format( "%s/%s/%s", tostring(dev), tostring(sid), tostring(var))
local del = {}
for _,v in pairs( pluginWatches[key] or {} ) do
if v.dev == PFB.device and ( func==nil or func==v.func ) then
table.insert( del, v.id )
end
end
for _,v in ipairs( del ) do pluginWatches[key][v] = nil end
end
local function pluginIsOpenLuup()
if pluginFlags.openluup == nil then
pluginFlags.openluup = false
for _,v in pairs( luup.devices ) do
if v.device_type == "openLuup" and v.device_num_parent == 0 then
pluginFlags.openluup = true
end
end
end
return pluginFlags.openluup
end
function pluginSetLogLevel( pdev, parms )
P("actionSetLogLevel(%1,%2)", pdev, parms)
PFB.loglevel = tonumber( parms.NewLogLevl ) or PFB.LOGLEVEL[parms.NewLogLevel] or PFB.LOGLEVEL.DEFAULT
setVar( pluginSID, "LogLevel", PFB.loglevel, pdev )
end
function pluginInitChild( child, prnt )
if setVar( "Configured", "1", child ) ~= "1" then
P("one-time initialization for %1 (child of %2)", child, prnt)
if type( pluginModule.childRunOnce ) == "function" then
pluginModule.childRunOnce( child, prnt )
end
end
local success,ret = pcall( pluginModule.childStart, child, prnt )
if not success then
L(logLevels.err, "Child %2 startup implementation failed: %1", ret, child)
luup.set_failure( 1, child )
return false, "Startup implementation failed", pluginModule._PLUGIN_NAME
end
luup.set_failure( ret ~= false and 0 or 1, child )
return true
end
function pluginStart(dev)
if pluginFlags.debug then luup.log("Using PluginFrameworkBasic "..PFB.VERSION.." (rigpapa) https://github.com/toggledbits/PluginTools") end
L('notice', "starting device #%1", dev)
PFB.device = dev
initVar( "LogLevel", PFB.LOGLEVEL.DEFAULT, dev, pluginSID )
PFB.loglevel = getVarNumeric( "LogLevel", PFB.LOGLEVEL.DEFAULT, dev, pluginSID )
pluginDevice = dev
pluginWatches = {}
pluginTimers = {}
if setVar("Configured", "1") ~= "1" then
P("one-time initialization for %1", dev)
initVar( "DebugMode", 0 )
pluginModule.runOnce( dev )
end
if getVarNumeric( "DebugMode", 0 ) ~= 0 then
PFB.loglevel = logLevels['debug1']
L(logLevels['debug1'], "Plugin debug enabled by state variable DebugMode")
end
-- Check firmware version
P("checking system version")
local vok,msg = pluginModule.checkVersion( dev )
if vok == false then
L('err', msg or "This plugin does not run on this firmware.")
luup.set_failure( 1, dev )
return false, msg or "Incompatible firmware", pluginModule._PLUGIN_NAME
end
-- Register plugin request handler
luup.register_handler("_RequestCb", pluginReqName)
P("calling module start")
local success,ret,msg = pcall( pluginModule.start, dev )
if not success then
L(logLevels.err, "Startup implementation failed: %1", ret)
luup.set_failure( 1, dev )
return false, "Startup implementation failed", pluginModule._PLUGIN_NAME
end
luup.set_failure( ret ~= false and 0 or 1, dev )
-- Start children
for n,d in childIterator( {}, dev ) do
pluginTimer( 0, pluginInitChild, n, dev )
end
-- Final return
return ret ~= false, msg or "", pluginModule._PLUGIN_NAME
end
-- _WatchCb is the callback function that will dispatch to the module
function _WatchCb( dev, svc, var, oldVal, newVal )
P("_WatchCb(%1,%2,%3,%4,%5)", dev, svc, var, oldVal, newVal)
local key = string.format( "%d/%s/%s", dev, svc, var )
for _,d in pairs( pluginWatches[key] or {} ) do
P("_WatchCb() dispatching watch event %1 to %2", d.id, d.dev)
local success,err = pcall( d.func, dev, svc, var, oldVal, newVal, d.dev, unpack( d.args or {} ) )
if not success then
L(logLevels.err, "Watch handler failed: %1", err)
end
end
end
-- _DelayCb is the callback function that will dispatch to the module
function _DelayCb( parm )
P("_DelayCb(%1)", parm)
if tonumber( parm ) ~= pluginMasterSerial then
P("_DelayCb() another timer sequence has started (that's OK); serial expected %1 got %2", pluginMasterSerial, parm)
return
end
execTimers( parm )
end
-- _RequestCb is the request handler; hands off to module function
function _RequestCb( req, parms, of )
P("_RequestCb(%1,%2,%3)", req, parms, of)
-- Built-in handler for turning debug on and off.
if parms.debug then
local n = tonumber( parms.debug )
if n then
debugMode=n~=0
else
debugMode=not debugMode
end
return '{"debug":'..tostring(debugMode)..',"PFB":'..PFB.VERSION..'}','application/json'
end
if type( pluginModule.handleRequest ) ~= "function" then return "ERROR\r\nRequest handler not implemented", "text/plain" end
return pluginModule.handleRequest( req, parms, of, pluginDevice )
end
local function pluginJobWatch( devnum, jobnum, func, ... )
P("pluginJobWatch(%1,%2,%3,...)", devnum, jobnum, func)
local key = tostring( "%s;%s;%s", tostring(devnum), tostring(jobnum), tostring( func ) )
if not next(pluginJobs or {}) then
pluginJobs = {}
luup.job_watch( '_JobCb' ) -- no device filter
end
pluginJobs[key] = { id=key, device=devnum, jobnum=jobnum, func=func, args=arg }
end
function _JobCb( jbt )
P("_JobCb(%1)", jbt)
for _,d in pairs( pluginJobs or {} ) do
if ( d.device==nil or d.device==jbt.device_num ) then
local s,e = pcall( d.func, tonumber(jbt.notes) or nil, jbt, unpack( d.args or {} ) )
if not s then
L('err', "Job watch handler threw an error: %1; job data=%2", e, jbt)
end
end
end
end
function childIterator( attr, prnt )
attr = attr or {}
prnt = prnt or PFB.device
local prev = nil
return function()
while true do
local n, d = next( luup.devices, prev )
prev = n
if n == nil then return nil end
local matched = d.device_num_parent == prnt
if matched then
for a,v in pairs( attr ) do
if d[a] ~= v then
matched = false
break
end
end
end
if matched then return n,d end
end
end
end
-- Exports for standard objects
PFB = {}
PFB.VERSION = 19184
PFB.device = false
PFB.log = L
PFB.LOGLEVEL = logLevels
PFB.loglevel = PFB.LOGLEVEL.DEFAULT
PFB.var = { get=getVar, getNumeric=getVarNumeric, set=setVar, init=initVar, delete=deleteVar }
PFB.delay = { once=pluginTimer, interval=pluginInterval, cancel=pluginCancelTimer }
PFB.watch = { set=pluginDeviceWatch, clear=pluginDeviceWatchClear }
PFB.job = { watch=pluginJobWatch, clear=pluginJobWatchClear, register=pluginTagJob }
PFB.isOpenLuup = pluginIsOpenLuup
PFB.matchChildren = childIterator
-- Expansion objects will go here
-- Load plugin implementation module
_,pluginModule = pcall( require, PLUGIN_MODULE_NAME )
-- if not package.loaded[PLUGIN_MODULE_NAME] then
if type( pluginModule ) ~= "table" then
error( "Cannot load plugin implementation module "..PLUGIN_MODULE_NAME..": ".._tostring(pluginModule) )
end
debugMode = false
pluginDevice = nil
pluginWatches = {}
pluginTimers = {}
pluginNextTID = 0
pluginMasterSerial = 0
pluginFlags = { debug=debugMode }
pluginName = pluginModule._PLUGIN_NAME
pluginIdent = pluginModule._PLUGIN_COMPACT or pluginModule._NAME:gsub("^L_", ""):gsub("1$", "")
pluginReqName = pluginModule._PLUGIN_REQUESTNAME or pluginIdent
pluginSID = pluginModule.MYSID or "urn:toggledbits-com:serviceId:Submasters1"
pluginModule.PFB = PFB
--[[ ===== D O N O T M O D I F Y A B O V E T H I S L I N E --]]
</functions>
<startup>pluginStart</startup><!-- DO NOT MODIFY -->
<actionList>
<!-- !!! Place your actions here. Generally, action implementations should be as small
as possible, ideally just a call to a module function... -->
<action>
<serviceId>urn:toggledbits-com:serviceId:Submasters1</serviceId>
<name>Example</name>
<run>
-- This is about all you should ever need in this file--just pass it on!
-- Your loaded module is stored in pluginModule
return pluginModule.actionExample( lul_device, lul_settings )
-- alternately, you could use this syntax, using the module name directly
-- return L_Submasters1.actionExample( lul_device, lul_settings )
</run>
</action>
<!-- SetLogLevel -- defined in default service, DO NOT REMOVE -->
<action>
<serviceId>urn:toggledbits-com:serviceId:Submasters1</serviceId>
<name>SetLogLevel</name>
<run>
return pluginSetDebug( lul_device, lul_settings ) -- note this is a local function
</run>
</action>
</actionList>
</implementation>