From 48d9adbaff6e63eb786f0f1e5983c246665b2437 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Sat, 26 Mar 2016 02:16:37 +0300 Subject: [PATCH] Scripting in presets User defined scripts can now be used in presets allowing dynamic values and complex provisioning workflow. --- lib/cache.coffee | 175 ++++++++++++++++++++++++++++++++++++++++++ lib/cwmp.coffee | 13 +++- lib/db.coffee | 10 ++- lib/nbi.coffee | 37 +++++++++ lib/provisions.coffee | 46 ----------- lib/sandbox.coffee | 121 +++++++++++++++++++++++++++++ lib/session.coffee | 156 +++++++++++++++++++++---------------- 7 files changed, 441 insertions(+), 117 deletions(-) create mode 100644 lib/cache.coffee delete mode 100644 lib/provisions.coffee create mode 100644 lib/sandbox.coffee diff --git a/lib/cache.coffee b/lib/cache.coffee new file mode 100644 index 0000000..3d77608 --- /dev/null +++ b/lib/cache.coffee @@ -0,0 +1,175 @@ +### +# Copyright 2013-2016 Zaid Abdulla +# +# GenieACS is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# GenieACS is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with GenieACS. If not, see . +### + +vm = require 'vm' +crypto = require 'crypto' + +db = require './db' + +REFRESH = 3000 + +nextRefresh = 0 +hash = null +presets = null +scripts = null + + +UNLOCK_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end' +EXTEND_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end' + +lock = (lockName, ttl, callback) -> + token = Math.random().toString(36).slice(2) + unlockOrExtend = (ttl) -> + if not ttl + db.redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) -> + throw err or new Error('Lock expired') if err or not res + ) + else + db.redisClient.eval(EXTEND_SCRIPT, 1, lockName, token, ttl, (err, res) -> + throw err or new Error('Lock expired') if err or not res + ) + return + + db.redisClient.set(lockName, token, 'NX', 'PX', ttl, (err, res) -> + if err or res + return callback(err, unlockOrExtend) + + setTimeout(() -> + return lock(lockName, ttl, callback) + , 200) + ) + + +computeHash = () -> + # MD5 hash for presets and scripts for detecting changes + h = crypto.createHash('md5') + for p in presets + h.update(JSON.stringify(p.name)) + h.update(JSON.stringify(p.precondition)) + h.update(JSON.stringify(p.provisions)) + + for s in Object.keys(scripts).sort() + h.update(JSON.stringify(s)) + h.update(JSON.stringify(scripts[s].source)) + + return h.digest('hex') + + +refresh = (callback) -> + now = Date.now() + db.redisClient.get('presets_hash', (err, res) -> + return callback(err) if err + + if hash? and res == hash + nextRefresh = now + REFRESH + return callback() + + lock('presets_hash_lock', 3000, (err, unlockOrExtend) -> + counter = 2 + db.presetsCollection.find().toArray((err, res) -> + if err + callback(err) if -- counter >= 0 + counter = 0 + return + + res.sort((a, b) -> + if a.weight == b.weight + return a._id > b._id + else + return a.weight - b.weight + ) + + presets = [] + + for preset in res + precondition = JSON.parse(preset.precondition) + + provisions = preset.provisions or [] + + # Generate provisions from the old configuration format + for c in preset.configurations + switch c.type + when 'age' + provisions.push(['refresh', c.name, c.age * -1000]) + when 'value' + provisions.push(['value', c.name, c.value]) + when 'add_tag' + provisions.push(['tag', c.tag, true]) + when 'delete_tag' + provisions.push(['tag', c.tag, false]) + when 'script' + provisions.push([c.name].concat(c.args or [])) + else + throw new Error("Unknown configuration type #{c.type}") + + presets.push({name: preset._id, precondition: precondition, provisions: provisions}) + + if -- counter == 0 + computeHash() + db.redisClient.setex("presets_hash", 300, hash, (err) -> + unlockOrExtend(0) + nextRefresh = now + REFRESH + return callback() + ) + ) + + db.scriptsCollection.find().toArray((err, res) -> + if err + callback(err) if -- counter >= 0 + counter = 0 + return + + scripts = {} + for r in res + scripts[r._id] = {} + scripts[r._id].source = r.source + scripts[r._id].md5 = r.md5 + scripts[r._id].compiled = vm.Script(r.source, {filename: r._id, timeout: 50}) + + if -- counter == 0 + computeHash() + db.redisClient.setex("presets_hash", 300, hash, (err) -> + unlockOrExtend(0) + nextRefresh = now + REFRESH + return callback() + ) + ) + ) + ) + + + +getPresets = (callback) -> + if Date.now() < nextRefresh + return callback(null, hash, presets) + + refresh((err) -> + return callback(err, hash, presets) + ) + + +getScripts = (callback) -> + if Date.now() < nextRefresh + return callback(null, hash, scripts) + + refresh((err) -> + return callback(err, hash, scripts) + ) + + +exports.getPresets = getPresets +exports.getScripts = getScripts diff --git a/lib/cwmp.coffee b/lib/cwmp.coffee index 970b62f..c35faf3 100644 --- a/lib/cwmp.coffee +++ b/lib/cwmp.coffee @@ -44,10 +44,10 @@ zlib = require 'zlib' config = require './config' common = require './common' soap = require './soap' -db = require './db' session = require './session' query = require './query' device = require './device' +cache = require './cache' writeResponse = (currentRequest, res) -> @@ -104,7 +104,11 @@ inform = (currentRequest, cwmpRequest) -> applyPresets = (currentRequest) -> - db.getPresetsObjectsAliases((presets, objects, aliases) -> + cache.getPresets((err, presetsHash, presets) -> + throw err if err + + currentRequest.sessionData.presetsHash = presetsHash + parameters = {} for p in presets for a in Object.keys(query.queryProjection(p.precondition)) @@ -126,10 +130,12 @@ applyPresets = (currentRequest) -> for p in presets if query.test(parameters, p.precondition) - session.addPreset(currentRequest.sessionData, p._id, p) + session.addProvisions(currentRequest.sessionData, p.provisions) session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest) -> throw err if err + if not rpcRequest? + session.clearProvisions(currentRequest.sessionData) sendRpcRequest(currentRequest, id, rpcRequest) ) ) @@ -140,6 +146,7 @@ nextRpc = (currentRequest) -> session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest) -> throw err if err if not rpcRequest? + session.clearProvisions(currentRequest.sessionData) return applyPresets(currentRequest) sendRpcRequest(currentRequest, id, rpcRequest) diff --git a/lib/db.coffee b/lib/db.coffee index 4e91799..f7adf69 100644 --- a/lib/db.coffee +++ b/lib/db.coffee @@ -26,10 +26,11 @@ tasksCollection = null devicesCollection = null presetsCollection = null objectsCollection = null +scriptsCollection = null connect = (callback) -> - callbackCounter = 6 + callbackCounter = 7 mongodb.MongoClient.connect(config.get('MONGODB_CONNECTION_URL'), {db:{w:1},server:{autoReconnect:true}}, (err, db) -> return callback(err) if err exports.mongoDb = db @@ -71,6 +72,13 @@ connect = (callback) -> return callback(err) ) + db.collection('scripts', (err, collection) -> + exports.scriptsCollection = scriptsCollection = collection + if --callbackCounter == 0 or err + callbackCounter = 0 + return callback(err) + ) + exports.redisClient = redisClient = redis.createClient(config.get('REDIS_PORT'), config.get('REDIS_HOST')) redisClient.select(config.get('REDIS_DB'), (err) -> if --callbackCounter == 0 or err diff --git a/lib/nbi.coffee b/lib/nbi.coffee index 6ba49c3..2f19cd5 100644 --- a/lib/nbi.coffee +++ b/lib/nbi.coffee @@ -41,6 +41,7 @@ url = require 'url' mongodb = require 'mongodb' querystring = require 'querystring' +crypto = require 'crypto' config = require './config' common = require './common' @@ -58,6 +59,7 @@ FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~ PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.]+)\/?$/ QUERY_REGEX = /^\/([a-zA-Z0-9_]+s)\/?$/ DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/ +SCRIPTS_REGEX = /^\/scripts\/([a-zA-Z0-9\-\_\%]+)\/?$/ errorToString = (err) -> @@ -149,6 +151,41 @@ listener = (request, response) -> else response.writeHead 405, {'Allow': 'PUT, DELETE'} response.end('405 Method Not Allowed') + else if SCRIPTS_REGEX.test(urlParts.pathname) + scriptName = querystring.unescape(SCRIPTS_REGEX.exec(urlParts.pathname)[1]) + if request.method == 'PUT' + object = { + _id: scriptName + source: body.toString() + md5: crypto.createHash('md5').update(body).digest('hex') + } + + db.scriptsCollection.save(object, (err) -> + db.redisClient.del('presets_hash', (err) -> + throw err if err + ) + if err + response.writeHead(500) + response.end(errorToString(err)) + return + response.writeHead(200) + response.end() + ) + else if request.method == 'DELETE' + db.scriptsCollection.remove({'_id' : scriptName}, (err) -> + db.redisClient.del('presets_hash', (err) -> + throw err if err + ) + if err + response.writeHead(500) + response.end(errorToString(err)) + return + response.writeHead(200) + response.end() + ) + else + response.writeHead 405, {'Allow': 'PUT, DELETE'} + response.end('405 Method Not Allowed') else if TAGS_REGEX.test(urlParts.pathname) r = TAGS_REGEX.exec(urlParts.pathname) deviceId = querystring.unescape(r[1]) diff --git a/lib/provisions.coffee b/lib/provisions.coffee deleted file mode 100644 index c266ba8..0000000 --- a/lib/provisions.coffee +++ /dev/null @@ -1,46 +0,0 @@ -### -# Copyright 2013-2016 Zaid Abdulla -# -# GenieACS is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# GenieACS is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with GenieACS. If not, see . -### - -common = require './common' - - -processProvision = (type, args, callback) -> - switch type - when 'refresh' - declarations = [] - path = common.parsePath(args[0]) - declaration = [args[1], null, null, null, null, null, args[1]] - for i in [path.length...14] by 1 - path = path.slice() - path.length = i - declarations.push([path].concat(declaration)) - - return callback(null, declarations) - - when 'value' - declarations = [[common.parsePath(args[0]), args[1], null, null, null, null, null, args[1]]] - return callback(null, declarations) - - when 'tag' - declarations = [[['Tags', args[0]], null, null, null, null, null, null, null, [args[1], 'xsd:boolean']]] - return callback(null, declarations) - - else - return callback(new Error("Unknown provision type '#{type}'")) - - -exports.processProvision = processProvision diff --git a/lib/sandbox.coffee b/lib/sandbox.coffee new file mode 100644 index 0000000..cd3b6a9 --- /dev/null +++ b/lib/sandbox.coffee @@ -0,0 +1,121 @@ +### +# Copyright 2013-2016 Zaid Abdulla +# +# GenieACS is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# GenieACS is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with GenieACS. If not, see . +### + +vm = require 'vm' + +common = require './common' +device = require './device' + + +# Used for throwing to exit user script +class Declare; + + +sandbox = { + deviceData: null + args: null + revision: null + maxRevision: null + declarations: null + context: vm.createContext() +} + +sandbox.context.declare = (decs) -> + if ++ sandbox.revision > sandbox.maxRevision + 1 + throw new Error('Declare function should not be called from within a try/catch block') + + sandbox.declarations = decs + + if sandbox.revision == sandbox.maxRevision + 1 + throw new Declare() + + res = {} + for k, v of decs + path = common.parsePath(k) + all = device.getAll(sandbox.deviceData, path, sandbox.revision) + res[k] = {} + for a in all + r = {} + r.exist = a[1] if v.exist? + r.type = a.slice(3, 5) if v.type? + r.writable = a.slice(5, 7) if v.writable? + r.value = a.slice(7, 9) if v.value? and a[8]? + + p = a[0].join('.') + if p == k + res[k] = r + else + res[k][p] = r + + return res + +Object.freeze(sandbox.context) + + +# Convert from friendly declaration format +toIndexDeclaration = (keyDeclaraiton) -> + indexDeclaraiton = [] + + if keyDeclaraiton['exist']? + if Array.isArray(keyDeclaraiton['exist']) + indexDeclaraiton[0] = keyDeclaraiton['exist'][0] + else + indexDeclaraiton[0] = keyDeclaraiton['exist'] + + if keyDeclaraiton['type']? + if Array.isArray(keyDeclaraiton['type']) + indexDeclaraiton[2] = keyDeclaraiton['type'][0] + else + indexDeclaraiton[2] = keyDeclaraiton['type'] + + if keyDeclaraiton['writable']? + if Array.isArray(keyDeclaraiton['writable']) + indexDeclaraiton[4] = keyDeclaraiton['writable'][0] + else + indexDeclaraiton[4] = keyDeclaraiton['writable'] + + if keyDeclaraiton['value']? + if Array.isArray(keyDeclaraiton['value']) + indexDeclaraiton[6] = keyDeclaraiton['value'][0] + indexDeclaraiton[7] = keyDeclaraiton['value'][1] + else + indexDeclaraiton[6] = keyDeclaraiton['value'] + + return indexDeclaraiton + + +run = (script, args, deviceData, revision, declarations) -> + sandbox.deviceData = deviceData + sandbox.args = args + sandbox.maxRevision = revision + sandbox.revision = 0 + sandbox.declarations = [] + + try + script.runInNewContext(sandbox.context) + catch err + throw err if err not instanceof Declare + for k, v of sandbox.declarations + declarations.push([common.parsePath(k)].concat(toIndexDeclaration(v))) + return true + + for k, v of sandbox.declarations + declarations.push([common.parsePath(k)].concat(toIndexDeclaration(v))) + return false + + +exports.run = run diff --git a/lib/session.coffee b/lib/session.coffee index 2df0197..303a945 100644 --- a/lib/session.coffee +++ b/lib/session.coffee @@ -21,7 +21,8 @@ config = require './config' common = require './common' db = require './db' device = require './device' -provisions = require './provisions' +sandbox = require './sandbox' +cache = require './cache' init = (deviceId, cwmpVersion, timeout) -> @@ -35,8 +36,8 @@ init = (deviceId, cwmpVersion, timeout) -> deviceData : deviceData cwmpVersion : cwmpVersion timeout : timeout - presets : {} - rpcCount : 0 + provisions: [] + counter : [0, 0, 0] # rpc, revision, batch } return sessionData @@ -65,26 +66,47 @@ loadParameters = (sessionData, toLoad, callback) -> ) -inform = (sessionData, rpcRequest, callback) -> - timestamp = sessionData.timestamp - device.set(sessionData.deviceData, ['DeviceID', 'Manufacturer'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.Manufacturer, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'OUI'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.OUI, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'ProductClass'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.ProductClass, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'SerialNumber'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.SerialNumber, 'xsd:string']]) +generateRpcId = (sessionData) -> + if sessionData.counter[0] > 255 or sessionData.counter[1] > 15 or sessionData.counter[2] > 15 + throw new Error('Too many RPCs') - for p in rpcRequest.parameterList + return sessionData.counter[2].toString(16).slice(-1) + + sessionData.counter[1].toString(16).slice(-1) + + ('0' + sessionData.counter[0].toString(16)).slice(-2) + + +inform = (sessionData, rpcReq, callback) -> + timestamp = sessionData.timestamp + device.set(sessionData.deviceData, ['DeviceID', 'Manufacturer'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.Manufacturer, 'xsd:string']]) + device.set(sessionData.deviceData, ['DeviceID', 'OUI'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.OUI, 'xsd:string']]) + device.set(sessionData.deviceData, ['DeviceID', 'ProductClass'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.ProductClass, 'xsd:string']]) + device.set(sessionData.deviceData, ['DeviceID', 'SerialNumber'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.SerialNumber, 'xsd:string']]) + + for p in rpcReq.parameterList device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)]) device.set(sessionData.deviceData, ['Events', 'Inform'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']]) - for e in rpcRequest.event + for e in rpcReq.event device.set(sessionData.deviceData, ['Events', e.replace(' ', '_')], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']]) return callback(null, {type : 'InformResponse'}) -addPreset = (sessionData, name, preset) -> - sessionData.presets[name] = preset +addProvisions = (sessionData, provisions) -> + sessionData.provisions = sessionData.provisions.concat(provisions) + if sessionData.counter[1] > 0 + sessionData.counter[2] += 1 + sessionData.counter[1] = 0 + sessionData.counter[0] = 0 + + +clearProvisions = (sessionData) -> + sessionData.provisions = [] + if sessionData.counter[1] > 0 + sessionData.counter[2] += 1 + sessionData.counter[1] = 0 + sessionData.counter[0] = 0 generateRpcRequest = (sessionData, declarations, callback) -> @@ -214,26 +236,26 @@ generateRpcRequest = (sessionData, declarations, callback) -> return if res.gpn? - rpcRequest = { + rpcReq = { type: 'GetParameterNames' parameterPath: res.gpn[0].join('.') nextLevel: true } - return callback(null, rpcRequest) + return callback(null, rpcReq) if res.gpv? - rpcRequest = { + rpcReq = { type: 'GetParameterValues' parameterNames: (p.join('.') for p in res.gpv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId))) } - return callback(null, rpcRequest) + return callback(null, rpcReq) if res.spv? - rpcRequest = { + rpcReq = { type: 'SetParameterValues' parameterList: ([p[0].join('.'), p[1], p[2]] for p in res.spv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId))) } - return callback(null, rpcRequest) + return callback(null, rpcReq) return cb() @@ -263,74 +285,73 @@ generateRpcRequest = (sessionData, declarations, callback) -> rpcRequest = (sessionData, allDeclarations, callback) -> - presetNames = Object.keys(sessionData.presets) - presetNames.sort((a, b) -> - if sessionData.presets[a].weight == sessionData.presets[b].weight - return a > b - else - return sessionData.presets[a].weight - sessionData.presets[b].weight - ) - allDeclarations = allDeclarations?.slice() ? [] - counter = 1 - for presetName in presetNames - do (presetName) -> - for provision in sessionData.presets[presetName].provisions - ++ counter - provisions.processProvision(provision[0], provision[1..], (err, declarations) -> - if err - callback(err) if -- counter >= 0 - counter = 0 - return + nextRevision = false - allDeclarations = allDeclarations.concat(declarations) + cache.getScripts((err, presetsHash, scripts) -> + return callback(err) if err or presetsHash != sessionData.presetsHash - if -- counter == 0 - generateRpcRequest(sessionData, allDeclarations, (err, rpcRequest) -> - sessionData.rpcRequest = rpcRequest - if err or not rpcRequest? - return callback(err) + for provision in sessionData.provisions + if not scripts[provision[0]]? + switch provision[0] + when 'refresh' + path = common.parsePath(provision[1]) + for i in [path.length...16] by 1 + path.length = i + allDeclarations.push([path.slice(), provision[2], null, null, null, null, null, provision[2]]) + when 'value' + allDeclarations.push([[common.parsePath(provision[1]), provision[2], null, null, null, null, null, provision[2]]]) + when 'tag' + allDeclarations.push([[['Tags', provision[1]], null, null, null, null, null, null, null, [provision[2], 'xsd:boolean']]]) + continue - callback(err, sessionData.rpcCount++, rpcRequest) - ) - return - ) + nextRevision ||= sandbox.run(scripts[provision[0]].compiled, provision.slice(1), sessionData.deviceData, sessionData.counter[1], allDeclarations) - if -- counter == 0 - generateRpcRequest(sessionData, allDeclarations, (err, rpcRequest) -> - sessionData.rpcRequest = rpcRequest - if err or not rpcRequest? - return callback(err) + generateRpcRequest(sessionData, allDeclarations, (err, rpcReq) -> + sessionData.rpcRequest = rpcReq + return callback(err) if err - callback(err, sessionData.rpcCount++, rpcRequest) + if not rpcReq? + if not nextRevision + return callback() + sessionData.counter[0] = 0 + sessionData.counter[1] += 1 + return rpcRequest(sessionData, null, callback) + + callback(err, generateRpcId(sessionData), rpcReq) ) - return + ) + return -rpcResponse = (sessionData, id, rpcResponse, callback) -> - # TODO verify ID - rpcRequest = sessionData.rpcRequest +rpcResponse = (sessionData, id, rpcRes, callback) -> + if id != generateRpcId(sessionData) + return callback(new Error('Request ID not recognized')) + + ++ sessionData.counter[0] + + rpcReq = sessionData.rpcRequest sessionData.rpcRequest = null timestamp = sessionData.timestamp - switch rpcResponse.type + switch rpcRes.type when 'GetParameterValuesResponse' - return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'GetParameterValues' + return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterValues' - for p in rpcResponse.parameterList + for p in rpcRes.parameterList device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)]) when 'GetParameterNamesResponse' - return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'GetParameterNames' + return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterNames' - device.set(sessionData.deviceData, common.parsePath(rpcRequest.parameterPath).concat(null), 1, timestamp) + device.set(sessionData.deviceData, common.parsePath(rpcReq.parameterPath).concat(null), 1, timestamp) - for p in rpcResponse.parameterList + for p in rpcRes.parameterList if common.endsWith(p[0], '.') path = common.parsePath(p[0][0...-1]) - if not rpcRequest.nextLevel + if not rpcReq.nextLevel device.set(sessionData.deviceData, path.conact(null), 1, timestamp) device.set(sessionData.deviceData, path, 1, [timestamp, 1, timestamp, 1, timestamp, if p[1] then 1 else 0]) @@ -338,9 +359,9 @@ rpcResponse = (sessionData, id, rpcResponse, callback) -> device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, timestamp, if p[1] then 1 else 0]) when 'SetParameterValuesResponse' - return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'SetParameterValues' + return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'SetParameterValues' - for p in rpcRequest.parameterList + for p in rpcReq.parameterList device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, timestamp, 1, timestamp, p.slice(1)]) else @@ -410,7 +431,8 @@ end = (sessionData, callback) -> exports.init = init exports.inform = inform -exports.addPreset = addPreset +exports.addProvisions = addProvisions +exports.clearProvisions = clearProvisions exports.rpcRequest = rpcRequest exports.rpcResponse = rpcResponse exports.rpcFault = rpcFault