Files
genieacs/lib/session.coffee
T

1601 lines
58 KiB
CoffeeScript

###
# Copyright 2013-2017 Zaid Abdulla
#
# This file is part of GenieACS.
#
# 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 <http://www.gnu.org/licenses/>.
###
config = require './config'
common = require './common'
db = require './db'
device = require './device'
sandbox = require './sandbox'
localCache = require './local-cache'
PathSet = require './path-set'
VersionedMap = require './versioned-map'
InstanceSet = require './instance-set'
defaultProvisions = require './default-provisions'
MAX_ITERATIONS = 64
provisionsCache = new WeakMap()
virtualParametersCache = new WeakMap()
initDeviceData = () ->
return {
paths : new PathSet()
timestamps : new VersionedMap()
attributes : new VersionedMap()
loaded : new Map()
trackers : new Map()
changes : new Set()
}
init = (deviceId, cwmpVersion, timeout, callback) ->
timestamp = Date.now()
sessionContext = {
timestamp : timestamp
deviceId : deviceId
deviceData : initDeviceData()
cwmpVersion : cwmpVersion
timeout : timeout
provisions: []
channels: {}
virtualParameters: []
revisions: [0]
rpcCount: 0
iteration: 0
cycle: 0
extensionsCache: {}
declarations: []
}
localCache.getProvisionsAndVirtualParameters((err, hash, provisions, virtualParameters) ->
return callback(err) if err
sessionContext.presetsHash = hash
provisionsCache.set(sessionContext, provisions)
virtualParametersCache.set(sessionContext, virtualParameters)
return callback(null, sessionContext)
)
loadParameters = (sessionContext, callback) ->
if not sessionContext.toLoad?.length
return callback()
db.fetchDevice(sessionContext.deviceId, sessionContext.timestamp, sessionContext.toLoad, (err, parameters, loaded) ->
return callback(err) if err
if not parameters?
# Device not available in database, mark as new
sessionContext.new = true
loaded = [[[], (1 << config.get('MAX_DEPTH', sessionContext.deviceId)) - 1]]
parameters = []
for p in loaded
path = sessionContext.deviceData.paths.add(p[0])
if p[1]
l = sessionContext.deviceData.loaded.get(path) | 0
sessionContext.deviceData.loaded.set(path, l | p[1])
for p in parameters
path = sessionContext.deviceData.paths.add(p[0])
sessionContext.deviceData.timestamps.set(path, p[1], 0)
if p[2]
sessionContext.deviceData.attributes.set(path, p[2], 0)
delete sessionContext.toLoad
return callback()
)
generateRpcId = (sessionContext) ->
return sessionContext.timestamp.toString(16) +
sessionContext.cycle.toString(16).slice(-1) +
"0#{sessionContext.rpcCount.toString(16)}".slice(-2)
inform = (sessionContext, rpcReq, callback) ->
timestamp = sessionContext.timestamp + sessionContext.iteration + 1
params = []
params.push([['DeviceID', 'Manufacturer'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.Manufacturer, 'xsd:string']]}])
params.push([['DeviceID', 'OUI'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.OUI, 'xsd:string']]}])
params.push([['DeviceID', 'ProductClass'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.ProductClass, 'xsd:string']]}])
params.push([['DeviceID', 'SerialNumber'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [rpcReq.deviceId.SerialNumber, 'xsd:string']]}])
for p in rpcReq.parameterList
path = common.parsePath(p[0])
params.push([path, timestamp, {object: [timestamp, 0], value: [timestamp, p.slice(1)]}])
params.push([['Events', 'Inform'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [timestamp, 'xsd:dateTime']]}])
for e in rpcReq.event
params.push([['Events', e.replace(' ', '_')], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [timestamp, 'xsd:dateTime']]}])
for p in params
loadPath(sessionContext, p[0])
loadParameters(sessionContext, (err) ->
return callback(err) if err
if sessionContext.new
params.push([['Events', 'Registered'], timestamp,
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [timestamp, 'xsd:dateTime']]}])
sessionContext.deviceData.timestamps.revision = 1
sessionContext.deviceData.attributes.revision = 1
toClear = null
for p in params
toClear = device.set(sessionContext.deviceData, p[0], p[1], p[2], toClear)
clear(sessionContext, toClear, (err) ->
return callback(err, {name: 'InformResponse'})
)
)
transferComplete = (sessionContext, rpcReq, callback) ->
revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
sessionContext.deviceData.timestamps.revision = revision
sessionContext.deviceData.attributes.revision = revision
commandKey = rpcReq.commandKey
operation = sessionContext.operations[commandKey]
if not operation?
return callback(null, {name: 'TransferCompleteResponse'})
instance = operation.args.instance
delete sessionContext.operations[commandKey]
sessionContext.operationsTouched ?= {}
sessionContext.operationsTouched[commandKey] = 1
if rpcReq.faultStruct? and rpcReq.faultStruct.faultCode != '0'
return revertDownloadParameters(sessionContext, operation.args.instance, (err) ->
fault = {
code: "cwmp.#{rpcReq.faultStruct.faultCode}"
message: rpcReq.faultStruct.faultString
detail: rpcReq.faultStruct
timestamp: operation.timestamp
}
return callback(err, {name: 'TransferCompleteResponse'}, operation, fault)
)
loadPath(sessionContext, ['Downloads', instance, '*'])
loadParameters(sessionContext, (err) ->
return callback(err) if err
toClear = null
timestamp = sessionContext.timestamp + sessionContext.iteration + 1
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastDownload'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [operation.timestamp, 'xsd:dateTime']]}, toClear)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastFileType'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [operation.args.fileType, 'xsd:string']]}, toClear)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastFileName'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [operation.args.fileName, 'xsd:string']]}, toClear)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastTargetFileName'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [operation.args.targetFileName, 'xsd:string']]}, toClear)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'StartTime'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [+rpcReq.startTime, 'xsd:dateTime']]}, toClear)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'CompleteTime'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [+rpcReq.completeTime, 'xsd:dateTime']]}, toClear)
clear(sessionContext, toClear, (err) ->
return callback(err, {name: 'TransferCompleteResponse'}, operation)
)
)
revertDownloadParameters = (sessionContext, instance, callback) ->
loadPath(sessionContext, ['Downloads', instance, '*'])
loadParameters(sessionContext, (err) ->
return callback(err) if err
timestamp = sessionContext.timestamp + sessionContext.iteration + 1
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'LastDownload'])
LastDownload = sessionContext.deviceData.attributes.get(p)
p = sessionContext.deviceData.paths.add(['Downloads', instance, 'Download'])
toClear = device.set(sessionContext.deviceData, p, timestamp,
{value: [timestamp, [LastDownload?.value[1]?[0] or 0, 'xsd:dateTime']]}, toClear)
clear(sessionContext, toClear, callback)
)
timeoutOperations = (sessionContext, callback) ->
revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
sessionContext.deviceData.timestamps.revision = revision
sessionContext.deviceData.attributes.revision = revision
faults = []
operations = []
counter = 3
for commandKey, operation of sessionContext.operations
if operation.name isnt 'Download'
return callback(new Error("Unknown operation name #{operation.name}"))
DOWNLOAD_TIMEOUT = config.get('DOWNLOAD_TIMEOUT', sessionContext.deviceId) * 1000
if sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT
delete sessionContext.operations[commandKey]
sessionContext.operationsTouched ?= {}
sessionContext.operationsTouched[commandKey] = 1
faults.push({
code: 'timeout'
message: 'Download operation timed out'
timestamp: operation.timestamp
})
operations.push(operation)
counter += 2
revertDownloadParameters(sessionContext, operation.args.instance, (err) ->
if err
callback(err) if counter & 1
return counter = 0
return callback(null, faults, operations) if (counter -= 2) == 1
)
return callback(null, faults, operations) if (counter -= 2) == 1
addProvisions = (sessionContext, channel, provisions) ->
delete sessionContext.syncState
delete sessionContext.rpcRequest
sessionContext.declarations = []
sessionContext.doneProvisions = 0
if sessionContext.revisions[sessionContext.revisions.length - 1] > 0
sessionContext.deviceData.timestamps.collapse(1)
sessionContext.deviceData.attributes.collapse(1)
sessionContext.revisions = [0]
sessionContext.extensionsCache = {}
if sessionContext.iteration != sessionContext.cycle * MAX_ITERATIONS
sessionContext.cycle += 1
sessionContext.rpcCount = 0
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS
sessionContext.channels[channel] |= 0
for provision, i in provisions
channels = [channel]
# Remove duplicate provisions
provisionStr = JSON.stringify(provision)
for p, j in sessionContext.provisions
if JSON.stringify(p) == provisionStr
sessionContext.provisions.splice(j, 1)
for c of sessionContext.channels
channels.push(c) if sessionContext.channels[c] & (1 << j)
a = sessionContext.channels[c] >> (j + 1)
sessionContext.channels[c] &= ((1 << j) - 1)
sessionContext.channels[c] |= (a << j)
for c in channels
sessionContext.channels[channel] |= (1 << sessionContext.provisions.length)
sessionContext.provisions.push(provision)
clearProvisions = (sessionContext) ->
if sessionContext.revisions[sessionContext.revisions.length - 1] > 0
sessionContext.deviceData.timestamps.collapse(1)
sessionContext.deviceData.attributes.collapse(1)
if sessionContext.iteration != sessionContext.cycle * MAX_ITERATIONS
sessionContext.cycle += 1
sessionContext.rpcCount = 0
sessionContext.iteration = sessionContext.cycle * MAX_ITERATIONS
delete sessionContext.syncState
delete sessionContext.rpcRequest
sessionContext.provisions = []
sessionContext.virtualParameters = []
sessionContext.channels = {}
sessionContext.declarations = []
sessionContext.doneProvisions = 0
sessionContext.revisions = [0]
sessionContext.extensionsCache = {}
runProvisions = (sessionContext, provisions, startRevision, endRevision, callback) ->
done = true
allDeclarations = []
allClear = []
counter = 3
allProvisions = provisionsCache.get(sessionContext)
for provision, j in provisions
if not allProvisions[provision[0]]?
allDeclarations[j] = []
allClear[j] = []
if defaultProvisions[provision[0]]
done = defaultProvisions[provision[0]](sessionContext, provision, allDeclarations[j], startRevision, endRevision) and done
continue
counter += 2
do (j) -> sandbox.run(allProvisions[provision[0]].script,
{args: provision.slice(1)}, sessionContext, startRevision, endRevision,
(err, _fault, _clear, _declarations, _done) ->
if err or _fault
callback(err, _fault) if counter & 1
return counter = 0
done &&= _done
allDeclarations[j] = _declarations or []
allClear[j] = _clear or []
if (counter -= 2) == 1
allDeclarations = Array.prototype.concat.apply([], allDeclarations)
allClear = Array.prototype.concat.apply([], allClear)
return callback(null, null, done, allDeclarations, allClear)
)
if (counter -= 2) == 1
allDeclarations = Array.prototype.concat.apply([], allDeclarations)
allClear = Array.prototype.concat.apply([], allClear)
return callback(null, null, done, allDeclarations, allClear)
runVirtualParameters = (sessionContext, provisions, startRevision, endRevision, callback) ->
done = true
virtualParameterUpdates = []
allDeclarations = []
allClear = []
counter = 3
allVirtualParameters = virtualParametersCache.get(sessionContext)
for provision, j in provisions
counter += 2
globals = {args: provision.slice(1)}
do (j) -> sandbox.run(allVirtualParameters[provision[0]].script, globals,
sessionContext, startRevision, endRevision,
(err, _fault, _clear, _declarations, _done, _returnValue) ->
if err or _fault
callback(err, _fault) if counter & 1
return counter = 0
done &&= _done
allDeclarations[j] = _declarations or []
allClear[j] = _clear or []
virtualParameterUpdates[j] = _returnValue if _done
if (counter -= 2) == 1
toClear = null
if done
for vpu, i in virtualParameterUpdates
toClear = commitVirtualParameter(sessionContext, provisions[i], vpu, toClear)
clear(sessionContext, toClear, (err) ->
allDeclarations = Array.prototype.concat.apply([], allDeclarations)
allClear = Array.prototype.concat.apply([], allClear)
return callback(err, null, done, allDeclarations, allClear)
)
)
if (counter -= 2) == 1
toClear = null
if done
for vpu, i in virtualParameterUpdates
toClear = commitVirtualParameter(sessionContext, provisions[i], vpu, toClear)
clear(sessionContext, toClear, (err) ->
allDeclarations = Array.prototype.concat.apply([], allDeclarations)
allClear = Array.prototype.concat.apply([], allClear)
return callback(err, null, done, allDeclarations, allClear)
)
runDeclarations = (sessionContext, declarations) ->
sessionContext.syncState ?= {
refreshAttributes: {
exist: new Set()
object: new Set()
writable: new Set()
value: new Set()
}
spv: new Map()
gpn: new Set()
gpnPatterns: new Map()
tags: new Map()
virtualParameterDeclarations: []
instancesToDelete: new Map()
instancesToCreate: new Map()
downloadsToDelete: new Set()
downloadsToCreate: new InstanceSet()
downloadsValues: new Map()
downloadsDownload: new Map()
}
allDeclareTimestamps = new Map()
allDeclareAttributeTimestamps = new Map()
allDeclareAttributeValues = new Map()
allVirtualParameters = virtualParametersCache.get(sessionContext)
for declaration, i in declarations
path = common.addPathMeta(declaration[0])
unpacked = null
if (path.alias | path.wildcard) & 1 or path[0] == 'VirtualParameters'
sessionContext.deviceData.paths.add(['VirtualParameters'])
if (path.alias | path.wildcard) & 2
sessionContext.deviceData.paths.add(['VirtualParameters', '*'])
for k of allVirtualParameters
sessionContext.deviceData.paths.add(['VirtualParameters', k])
if (path.alias | path.wildcard) & 1 or path[0] == 'Reboot'
sessionContext.deviceData.paths.add(['Reboot'])
if (path.alias | path.wildcard) & 1 or path[0] == 'FactoryReset'
sessionContext.deviceData.paths.add(['FactoryReset'])
if path.alias
aliasDecs = device.getAliasDeclarations(path, declaration[1] or 1)
for ad in aliasDecs
p = sessionContext.deviceData.paths.add(ad[0])
allDeclareTimestamps.set(p, Math.max(ad[1] or 1, allDeclareTimestamps.get(p) or 0))
attrTrackers = []
if ad[2]
cur = allDeclareAttributeTimestamps.get(p)
allDeclareAttributeTimestamps.set(p, cur = {}) if not cur
for k, v of ad[2]
attrTrackers.push(k)
cur[k] = Math.max(v, cur[k] or 0)
device.track(sessionContext.deviceData, p, 'prerequisite', false, null, true, attrTrackers)
unpacked = device.unpack(sessionContext.deviceData, path)
for u in unpacked
allDeclareTimestamps.set(u, Math.max(declaration[1] or 1, allDeclareTimestamps.get(u) or 0))
if declaration[2]
cur = allDeclareAttributeTimestamps.get(u)
allDeclareAttributeTimestamps.set(u, cur = {}) if not cur
for k, v of declaration[2]
cur[k] = Math.max(v, cur[k] or 0)
if declaration[4]
cur = allDeclareAttributeValues.get(u)
allDeclareAttributeValues.set(u, cur = {}) if not cur
for k, v of declaration[4]
cur[k] = v
else
path = sessionContext.deviceData.paths.add(path)
allDeclareTimestamps.set(path, Math.max(declaration[1] or 1, allDeclareTimestamps.get(path) or 0))
if declaration[2]
cur = allDeclareAttributeTimestamps.get(path)
allDeclareAttributeTimestamps.set(path, cur = {}) if not cur
for k, v of declaration[2]
cur[k] = Math.max(v, cur[k] or 0)
if declaration[4]
if declaration[4]
cur = allDeclareAttributeValues.get(path)
allDeclareAttributeValues.set(path, cur = {}) if not cur
for k, v of declaration[4]
cur[k] = v
device.track(sessionContext.deviceData, path, 'prerequisite', false, null, true)
if declaration[3]?
if Array.isArray(declaration[3])
minInstances = declaration[3][0]
maxInstances = declaration[3][1]
else
minInstances = maxInstances = declaration[3]
parent = common.addPathMeta(path.slice(0, -1))
if Array.isArray(path[path.length - 1])
keys = {}
for p, i in path[path.length - 1] by 2
keys[p] = path[path.length - 1][i + 1]
else if path[path.length - 1] == '*'
keys = {}
if ((path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1)) == 0
parent = sessionContext.deviceData.paths.add(parent)
unpacked ?= device.unpack(sessionContext.deviceData, path)
processInstances(sessionContext, parent, unpacked, keys, minInstances, maxInstances)
else
parentsUnpacked = device.unpack(sessionContext.deviceData, parent)
for parent in parentsUnpacked
parent = sessionContext.deviceData.paths.add(parent)
processInstances(sessionContext, parent, device.unpack(sessionContext.deviceData, common.addPathMeta(parent.concat([path[parent.length]]))), keys, minInstances, maxInstances)
return processDeclarations(sessionContext, allDeclareTimestamps, allDeclareAttributeTimestamps, allDeclareAttributeValues)
rpcRequest = (sessionContext, _declarations, callback) ->
if sessionContext.rpcRequest?
return callback(null, null, generateRpcId(sessionContext), sessionContext.rpcRequest)
if sessionContext.virtualParameters.length == 0 and
sessionContext.declarations.length == 0 and
not _declarations?.length and
(sessionContext.doneProvisions or sessionContext.provisions.length == 0)
return callback()
if sessionContext.declarations.length <= sessionContext.virtualParameters.length
inception = sessionContext.declarations.length
revision = sessionContext.revisions[inception] + 1
sessionContext.deviceData.timestamps.revision = revision
sessionContext.deviceData.attributes.revision = revision
if inception == 0
run = runProvisions
provisions = sessionContext.provisions
else
run = runVirtualParameters
provisions = sessionContext.virtualParameters[inception - 1]
return run(sessionContext, provisions, sessionContext.revisions[inception - 1] or 0, sessionContext.revisions[inception], (err, fault, done, decs, toClear) ->
return callback(err) if err
if fault
fault.timestamp = sessionContext.timestamp
return callback(null, fault)
sessionContext.declarations.push(decs)
sessionContext.doneProvisions |= 1 << inception if done
for d in decs
for ad in device.getAliasDeclarations(d[0], 1)
loadPath(sessionContext, ad[0])
return clear(sessionContext, toClear, (err) ->
return callback(err) if err
loadParameters(sessionContext, (err) ->
return callback(err) if err
rpcRequest(sessionContext, _declarations, callback)
)
)
)
if _declarations?.length
delete sessionContext.syncState
sessionContext.declarations[0] ?= []
sessionContext.declarations[0] = sessionContext.declarations[0].concat(_declarations)
for d in _declarations
for ad in device.getAliasDeclarations(d[0], 1)
loadPath(sessionContext, ad[0])
return loadParameters(sessionContext, (err) ->
return callback(err) if err
return rpcRequest(sessionContext, null, callback)
)
if sessionContext.rpcCount >= 255 or
sessionContext.revisions.length >= 8 or
sessionContext.cycle >= 16 or
sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)
fault = {
code: 'endless_cycle'
message: 'The provision seems to be repeating indefinitely'
timestamp: sessionContext.timestamp
}
return callback(null, fault)
if (sessionContext.syncState?.virtualParameterDeclarations?.length or 0) < sessionContext.declarations.length
inception = sessionContext.syncState?.virtualParameterDeclarations?.length or 0
# Avoid unnecessary increment of iteration when using vparams
sessionContext.iteration += 2 if inception == sessionContext.declarations.length - 1
vpd = runDeclarations(sessionContext, sessionContext.declarations[inception])
timestamp = sessionContext.timestamp + sessionContext.iteration
toClear = null
allVirtualParameters = virtualParametersCache.get(sessionContext)
vpd = vpd.filter((declaration) ->
if declaration[0].length == 1
if Object.keys(allVirtualParameters).length
toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, {object: [timestamp, 1], writable: [timestamp, 0]}, toClear)
else
toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
else if declaration[0].length == 2
if declaration[0][1] == '*'
for k, v of allVirtualParameters
toClear = device.set(sessionContext.deviceData, ['VirtualParameters', k], timestamp, {object: [timestamp, 0]}, toClear)
toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
else if not (declaration[0][1] of allVirtualParameters)
toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
else
return true
return false
)
return clear(sessionContext, toClear, (err) ->
return callback(err) if err
sessionContext.syncState.virtualParameterDeclarations[inception] = vpd
return rpcRequest(sessionContext, null, callback)
)
if not sessionContext.syncState?
return callback()
inception = sessionContext.declarations.length - 1
provisions = generateGetVirtualParameterProvisions(sessionContext, sessionContext.syncState.virtualParameterDeclarations[inception])
if not provisions
sessionContext.rpcRequest = generateGetRpcRequest(sessionContext)
if not sessionContext.rpcRequest
if sessionContext.deviceData.changes.has('prerequisite')
delete sessionContext.syncState
device.clearTrackers(sessionContext.deviceData, 'prerequisite')
return rpcRequest(sessionContext, null, callback)
toClear = null
timestamp = sessionContext.timestamp + sessionContext.iteration + 1
# Update tags
sessionContext.syncState.tags.forEach((v, p) ->
c = sessionContext.deviceData.attributes.get(p)
if v and not c?
toClear = device.set(sessionContext.deviceData, p, timestamp, {object: [timestamp, false], writable: [timestamp, true], value: [timestamp, [true, 'xsd:boolean']]}, toClear)
else if c? and not v
toClear = device.set(sessionContext.deviceData, p, timestamp, toClear)
noMoreTags = true
for p in sessionContext.deviceData.paths.find(['Tags', '*'], false, true)
if sessionContext.deviceData.attributes.has(p)
noMoreTags = false
break
if noMoreTags
toClear = device.set(sessionContext.deviceData, ['Tags'], timestamp, null, toClear)
)
# Downloads
index = null
sessionContext.syncState.downloadsToCreate.forEach((instance) ->
if not index?
index = 0
for p in sessionContext.deviceData.paths.find(['Downloads', '*'], false, true)
if +p[1] > index and sessionContext.deviceData.attributes.has(p)
index = +p[1]
++ index
toClear = device.set(sessionContext.deviceData, ['Downloads'],
timestamp,
{object: [timestamp, 1], writable: [timestamp, 1]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', "#{index}"],
timestamp,
{object: [timestamp, 1], writable: [timestamp, 1]}, toClear)
params = {
'FileType': {writable: 1, value: [instance.FileType or '', 'xsd:string']}
'FileName': {writable: 1, value: [instance.FileName or '', 'xsd:string']}
'TargetFileName': {writable: 1, value: [instance.TargetFileName or '', 'xsd:string']}
'Download': {writable: 1, value: [instance.Download or 0, 'xsd:dateTime']}
'LastFileType': {writable: 0, value: ['', 'xsd:string']}
'LastFileName': {writable: 0, value: ['', 'xsd:string']}
'LastTargetFileName': {writable: 0, value: ['', 'xsd:string']}
'LastDownload': {writable: 0, value: [0, 'xsd:dateTime']}
'StartTime': {writable: 0, value: [0, 'xsd:dateTime']}
'CompleteTime': {writable: 0, value: [0, 'xsd:dateTime']}
}
for k, v of params
toClear = device.set(sessionContext.deviceData, ['Downloads', "#{index}", k],
timestamp, {object: [timestamp, 0], writable: [timestamp, v.writable], value: [timestamp, v.value]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', "#{index}", '*'],
timestamp, null, toClear)
)
sessionContext.syncState.downloadsToCreate.clear()
sessionContext.syncState.downloadsToDelete.forEach((instance) ->
toClear = device.set(sessionContext.deviceData, instance, timestamp, null, toClear)
sessionContext.syncState.downloadsValues.forEach((v, p) ->
if p[1] == instance[1]
sessionContext.syncState.downloadsValues.delete(p)
)
)
sessionContext.syncState.downloadsToDelete.clear()
sessionContext.syncState.downloadsValues.forEach((v, p) ->
if attrs = sessionContext.deviceData.attributes.get(p)
if attrs.writable?[1] and attrs.value?
v = device.sanitizeParameterValue([v, attrs.value[1][1]])
if v[0] != attrs.value[1][0]
toClear = device.set(sessionContext.deviceData, p, timestamp, {value: [timestamp, v]}, toClear)
)
if toClear
return clear(sessionContext, toClear, (err) ->
return callback(err) if err
if sessionContext.deviceData.changes.has('prerequisite')
delete sessionContext.syncState
device.clearTrackers(sessionContext.deviceData, 'prerequisite')
return rpcRequest(sessionContext, null, callback)
)
if sessionContext.deviceData.changes.has('prerequisite')
delete sessionContext.syncState
device.clearTrackers(sessionContext.deviceData, 'prerequisite')
return rpcRequest(sessionContext, null, callback)
provisions = generateSetVirtualParameterProvisions(sessionContext, sessionContext.syncState.virtualParameterDeclarations[inception])
if not provisions
sessionContext.rpcRequest = generateSetRpcRequest(sessionContext)
if provisions
sessionContext.virtualParameters.push(provisions)
sessionContext.revisions.push(sessionContext.revisions[inception])
return rpcRequest(sessionContext, null, callback)
if sessionContext.rpcRequest
return callback(null, null, generateRpcId(sessionContext), sessionContext.rpcRequest)
++ sessionContext.revisions[inception]
sessionContext.declarations.pop()
sessionContext.syncState.virtualParameterDeclarations.pop()
if sessionContext.doneProvisions & (2 << (sessionContext.virtualParameters.length - 1))
doneProvisions = sessionContext.virtualParameters.pop()
sessionContext.revisions.pop()
sessionContext.doneProvisions &= (2 << sessionContext.virtualParameters.length) - 1
rev = sessionContext.revisions[sessionContext.revisions.length - 1]
sessionContext.deviceData.timestamps.collapse(rev + 1)
sessionContext.deviceData.attributes.collapse(rev + 1)
for k of sessionContext.extensionsCache
if rev < Number(k.split(':', 1)[0])
delete sessionContext.extensionsCache[k]
return rpcRequest(sessionContext, null, callback)
# Simple algorithm to estimate GPN count in a set of patterns used
# to decide whether to use nextLevel = false in GPN.
estimateGpnCount = (patterns) ->
res = []
counts = new Map()
for pattern in patterns
c = []
i = -1
while pattern[1] >> (++ i)
if pattern[1] & (1 << i)
f = (pattern[0].wildcard & pattern[1]) | ((pattern[1] >> pattern[0].length) << pattern[0].length)
r = [i, f & ((1 << i) - 1), 1, new Set()]
res.push(c[i] = r)
counts.set(pattern, c)
for pattern in patterns
pats = patterns.filter((pat) -> return pat != pattern)
i = -1
while pattern[1] >> (++ i)
pats = pats.filter((pat) ->
return false if not pat[1] << i
if not pat[0][i]? or pat[0][i] == '*' or pat[0][i] == pattern[0][i]
if pattern[1] & pat[1] & (1 << i)
counts.get(pattern)[i][3].add(counts.get(pat)[i])
return true
return false
)
count = 0
for r in res
div = 1
r[3].forEach((rr) -> ++ div if rr[3].has(r))
if r[3].size == div - 1
h = common.hammingWeight(r[1])
if h < 8
count += (r[2] * Math.pow(3, h)) / div
return Math.round(count)
generateGetRpcRequest = (sessionContext) ->
if not (syncState = sessionContext.syncState)?
return
iter = syncState.refreshAttributes.exist.values()
while (path = iter.next().value)
found = false
for p in sessionContext.deviceData.paths.find(path, false, true, 99)
if syncState.refreshAttributes.value.has(p) or
syncState.refreshAttributes.object.has(p) or
syncState.refreshAttributes.writable.has(p) or
syncState.gpn.has(p)
found = true
break
if not found
p = sessionContext.deviceData.paths.add(path.slice(0, -1))
syncState.gpn.add(p)
f = 1 << p.length
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
syncState.refreshAttributes.exist.clear()
iter = syncState.refreshAttributes.object.values()
while (path = iter.next().value)
found = false
for p in sessionContext.deviceData.paths.find(path, false, true, 99)
if syncState.refreshAttributes.value.has(p) or
(p.length > path.length and
(syncState.refreshAttributes.object.has(p) or
syncState.refreshAttributes.writable.has(p)))
found = true
break
if not found
p = sessionContext.deviceData.paths.add(path.slice(0, -1))
syncState.gpn.add(p)
f = 1 << p.length
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
syncState.refreshAttributes.object.clear()
iter = syncState.refreshAttributes.writable.values()
while (path = iter.next().value)
p = sessionContext.deviceData.paths.add(path.slice(0, -1))
syncState.gpn.add(p)
f = 1 << p.length
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p))
syncState.refreshAttributes.writable.clear()
if syncState.gpn.size
GPN_NEXT_LEVEL = config.get('GPN_NEXT_LEVEL', sessionContext.deviceId)
paths = Array.from(syncState.gpn.keys()).sort((a,b) -> b.length - a.length)
path = paths.pop()
while path and path.length and not sessionContext.deviceData.attributes.has(path)
syncState.gpn.delete(path)
path = paths.pop()
if path
patterns = []
for p in sessionContext.deviceData.paths.find(path, true, false, 99)
if v = syncState.gpnPatterns.get(p)
patterns.push([p, (v >> path.length) << path.length])
if path.length >= GPN_NEXT_LEVEL
est = estimateGpnCount(patterns)
else
est = 0
if est < Math.pow(2, Math.max(0, 8 - path.length))
nextLevel = true
syncState.gpn.delete(path)
else
nextLevel = false
for p in sessionContext.deviceData.paths.find(path, false, true, 99)
syncState.gpn.delete(p)
return {
name: 'GetParameterNames'
parameterPath: path.concat('').join('.')
nextLevel: nextLevel
}
if syncState.refreshAttributes.value.size
GPV_BATCH_SIZE = config.get('GPV_BATCH_SIZE', sessionContext.deviceId)
parameterNames = []
iter = syncState.refreshAttributes.value.values()
while (path = iter.next().value) and
parameterNames.length < GPV_BATCH_SIZE
syncState.refreshAttributes.value.delete(path)
if sessionContext.deviceData.attributes.has(path)
parameterNames.push(path)
if parameterNames.length
return {
name: 'GetParameterValues'
parameterNames: (p.join('.') for p in parameterNames)
}
return null
generateSetRpcRequest = (sessionContext) ->
if not (syncState = sessionContext.syncState)?
return
deviceData = sessionContext.deviceData
# Delete instance
iter = syncState.instancesToDelete.values()
while instances = iter.next().value
if (instance = instances.values().next().value) and
sessionContext.deviceData.attributes.has(instance)
return {
name: 'DeleteObject'
objectName: instance.concat('').join('.')
}
# Create instance
iter = syncState.instancesToCreate.entries()
while pair = iter.next().value
if sessionContext.deviceData.attributes.has(pair[0]) and
instance = pair[1].values().next().value
pair[1].delete(instance)
return {
name: 'AddObject'
objectName: pair[0].concat('').join('.')
instanceValues: instance
next: 'getInstanceKeys'
}
# Set values
GPV_BATCH_SIZE = config.get('GPV_BATCH_SIZE', sessionContext.deviceId)
parameterValues = []
syncState.spv.forEach((v, k) ->
return if parameterValues.length >= GPV_BATCH_SIZE
attrs = sessionContext.deviceData.attributes.get(k)
if (curVal = attrs.value?[1])? and attrs.writable?[1]
val = v.slice()
val[1] = curVal[1] if not val[1]?
device.sanitizeParameterValue(val)
if val[0] != curVal[0] or val[1] != curVal[1]
parameterValues.push([k, val[0], val[1]])
syncState.spv.delete(k)
)
if parameterValues.length
return {
name: 'SetParameterValues'
parameterList: ([p[0].join('.'), p[1], p[2]] for p in parameterValues)
}
# Download
iter = syncState.downloadsDownload.entries()
while pair = iter.next().value
if not (pair[1] <= deviceData.attributes.get(pair[0])?.value?[1][0])
fileTypePath = deviceData.paths.get(pair[0].slice(0, -1).concat('FileType'))
fileNamePath = deviceData.paths.get(pair[0].slice(0, -1).concat('FileName'))
targetFileNamePath = deviceData.paths.get(pair[0].slice(0, -1).concat('TargetFileName'))
return {
name: 'Download'
commandKey: generateRpcId(sessionContext)
instance: pair[0][1]
fileType: deviceData.attributes.get(fileTypePath)?.value?[1][0]
fileName: deviceData.attributes.get(fileNamePath)?.value?[1][0]
targetFileName: deviceData.attributes.get(targetFileNamePath)?.value?[1][0]
}
# Reboot
if syncState.reboot?
p = sessionContext.deviceData.paths.get(['Reboot'])
if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.reboot)
delete syncState.reboot
return {
name: 'Reboot'
}
# Factory reset
if syncState.factoryReset?
p = sessionContext.deviceData.paths.get(['FactoryReset'])
if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.factoryReset)
delete syncState.factoryReset
return {
name: 'FactoryReset'
}
return null
generateGetVirtualParameterProvisions = (sessionContext, virtualParameterDeclarations) ->
provisions = null
for declaration in virtualParameterDeclarations
if declaration[1]
provisions ?= []
currentTimestamps = {}
currentValues = {}
attrs = sessionContext.deviceData.attributes.get(declaration[0])
for k, v of attrs
currentTimestamps[k] = v[0]
currentValues[k] = v[1]
provisions.push([declaration[0][1], declaration[1], {}, currentTimestamps, currentValues])
delete declaration[1]
return provisions
generateSetVirtualParameterProvisions = (sessionContext, virtualParameterDeclarations) ->
provisions = null
for declaration in virtualParameterDeclarations
if declaration[2]?.value?
attrs = sessionContext.deviceData.attributes.get(declaration[0])
if (curVal = attrs.value?[1])? and attrs.writable?[1]
val = declaration[2].value.slice()
val[1] = curVal[1] if not val[1]?
device.sanitizeParameterValue(val)
if val[0] != curVal[0] or val[1] != curVal[1]
provisions ?= []
currentTimestamps = {}
currentValues = {}
for k, v of attrs
currentTimestamps[k] = v[0]
currentValues[k] = v[1]
provisions.push([declaration[0][1], {}, {value: val}, currentTimestamps, currentValues])
return provisions
processDeclarations = (sessionContext, allDeclareTimestamps, allDeclareAttributeTimestamps, allDeclareAttributeValues) ->
deviceData = sessionContext.deviceData
syncState = sessionContext.syncState
root = sessionContext.deviceData.paths.add([])
paths = sessionContext.deviceData.paths.find([], false, true, 99)
paths.sort((a, b) ->
if a.wildcard == b.wildcard
return a.length - b.length
return a.wildcard - b.wildcard
)
toClear = null
virtualParameterDeclarations = []
func = (leafParam, leafIsObject, leafTimestamp, paths) ->
currentPath = paths[0]
children = {}
declareTimestamp = 0
declareAttributeTimestamps = {}
declareAttributeValues = {}
currentTimestamp = 0
currentAttributes = null
if currentPath.wildcard == 0
currentAttributes = deviceData.attributes.get(currentPath)
for path in paths
if path.length > currentPath.length
fragment = path[currentPath.length]
if not children[fragment]
children[fragment] = []
if path.length > currentPath.length + 1
# This is to ensure we don't descend more than one step at a time
p = common.addPathMeta(path.slice(0, currentPath.length + 1))
children[fragment].push(p)
children[fragment].push(path)
continue
currentTimestamp = Math.max(currentTimestamp, deviceData.timestamps.get(path) ? 0)
declareTimestamp = Math.max(declareTimestamp, allDeclareTimestamps.get(path) ? 0)
if currentPath.wildcard == 0
if attrs = allDeclareAttributeTimestamps.get(path)
for attrName of attrs
declareAttributeTimestamps[attrName] = Math.max(attrs[attrName], declareAttributeTimestamps[attrName] or 0)
if attrs = allDeclareAttributeValues.get(path)
for attrName of attrs
declareAttributeValues[attrName] = attrs[attrName]
if currentAttributes
leafParam = currentPath
leafIsObject = currentAttributes.object?[1]
if leafIsObject == 0
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0])
else
leafTimestamp = Math.max(leafTimestamp, currentTimestamp)
switch (if currentPath[0] != '*' then currentPath[0] else leafParam[0])
when 'Reboot'
if currentPath.length == 1
if declareAttributeValues.value?
syncState.reboot = +(new Date(declareAttributeValues.value[0]))
when 'FactoryReset'
if currentPath.length == 1
if declareAttributeValues.value?
syncState.factoryReset = +(new Date(declareAttributeValues.value[0]))
when 'Tags'
if currentPath.length == 2 and currentPath.wildcard == 0 and declareAttributeValues?.value?
syncState.tags.set(currentPath, device.sanitizeParameterValue([declareAttributeValues.value[0], 'xsd:boolean'])[0])
when 'Events', 'DeviceID' then
# Do nothing
when 'Downloads'
if currentPath.length == 3 and currentPath.wildcard == 0 and declareAttributeValues.value?
if currentPath[2] == 'Download'
syncState.downloadsDownload.set(currentPath, declareAttributeValues.value[0])
else
syncState.downloadsValues.set(currentPath, declareAttributeValues.value[0])
when 'VirtualParameters'
if currentPath.length <= 2
d = null
if not (declareTimestamp <= currentTimestamp)
d = [currentPath]
if currentPath.wildcard == 0
if declareAttributeTimestamps
for attrName of declareAttributeTimestamps
if declareAttributeTimestamps[attrName] <= currentAttributes?[attrName]?[0]
delete declareAttributeTimestamps[attrName]
if Object.keys(declareAttributeTimestamps).length
d ?= [currentPath]
d[1] = declareAttributeTimestamps
if Object.keys(declareAttributeValues).length
d ?= [currentPath]
d[2] = declareAttributeValues
virtualParameterDeclarations.push(d) if d
else
if declareTimestamp > currentTimestamp and declareTimestamp > leafTimestamp
if leafIsObject
syncState.gpn.add(leafParam)
if leafTimestamp > 0
f = 1 << leafParam.length
syncState.gpnPatterns.set(leafParam, f | syncState.gpnPatterns.get(leafParam))
else
f = ((1 << currentPath.length) - 1) ^ ((1 << leafParam.length) - 1)
syncState.gpnPatterns.set(currentPath, f | syncState.gpnPatterns.get(currentPath))
else
syncState.refreshAttributes.object.add(leafParam)
if not leafIsObject?
f = ((1 << syncState.gpnPatterns.length) - 1) ^ ((1 << leafParam.length) - 1)
syncState.gpnPatterns.set(currentPath, f | syncState.gpnPatterns.get(currentPath))
if currentAttributes
for attrName of declareAttributeTimestamps
if not (declareAttributeTimestamps[attrName] <= currentAttributes[attrName]?[0])
if attrName == 'value'
if not currentAttributes.object?[1]?
syncState.refreshAttributes.object.add(currentPath)
else if currentAttributes.object[1] == 0
syncState.refreshAttributes.value.add(currentPath)
else
syncState.refreshAttributes[attrName].add(currentPath)
if declareAttributeValues.value?
syncState.spv.set(currentPath, declareAttributeValues.value)
for child of children
# This fine expression avoids duplicate visits
if ((currentPath.wildcard ^ children[child][0].wildcard) & ((1 << currentPath.length) - 1)) >> leafParam.length == 0
if child != '*' and children['*']?
children[child] = children[child].concat(children['*'])
func(leafParam, leafIsObject, leafTimestamp, children[child])
func(root, 1, 0, paths)
return virtualParameterDeclarations
loadPath = (sessionContext, path, depth) ->
return true if sessionContext.new
depth ?= (1 << path.length) - 1
# Trim trailing wildcards
trimWildcard = path.length
-- trimWildcard while trimWildcard and path[trimWildcard - 1] == '*'
path = path.slice(0, trimWildcard) if trimWildcard < path.length
for i in [0..path.length] by 1
for sup in sessionContext.deviceData.paths.find(path.slice(0, i), true, false)
v = sessionContext.deviceData.loaded.get(sup)
depth &= depth ^ sessionContext.deviceData.loaded.get(sup)
return true if depth == 0
sessionContext.toLoad ?= []
sessionContext.toLoad.push([path, depth])
return false
processInstances = (sessionContext, parent, parameters, keys, minInstances, maxInstances) ->
if parent[0] == 'Downloads'
return if parent.length != 1
instancesToDelete = sessionContext.syncState.downloadsToDelete
instancesToCreate = sessionContext.syncState.downloadsToCreate
else
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent)
if not instancesToDelete?
instancesToDelete = new Set()
sessionContext.syncState.instancesToDelete.set(parent, instancesToDelete)
instancesToCreate = sessionContext.syncState.instancesToCreate.get(parent)
if not instancesToCreate?
instancesToCreate = new InstanceSet()
sessionContext.syncState.instancesToCreate.set(parent, instancesToCreate)
counter = 0
for p in parameters
++ counter
if counter > maxInstances
instancesToDelete.add(p)
else if counter <= minInstances
instancesToDelete.delete(p)
superset = instancesToCreate.superset(keys)
for inst in superset
++ counter
if counter > maxInstances
instancesToCreate.delete(inst)
subset = instancesToCreate.subset(keys)
for inst in subset
++ counter
if counter <= minInstances
instancesToCreate.delete(inst)
instancesToCreate.add(JSON.parse(JSON.stringify(keys)))
while counter < minInstances
++ counter
instancesToCreate.add(JSON.parse(JSON.stringify(keys)))
return
clear = (sessionContext, toClear, callback) ->
return callback() if not toClear?.length
MAX_DEPTH = config.get('MAX_DEPTH', sessionContext.deviceId)
toClear.forEach((c) ->
loadPath(sessionContext, c[0], ((1 << c[0].length) - 1) ^ ((1 << MAX_DEPTH) - 1))
)
loadParameters(sessionContext, (err) ->
return callback(err) if err
toClear.forEach((c) ->
device.clear(sessionContext.deviceData, c[0], c[1], c[2])
)
return callback()
)
commitVirtualParameter = (sessionContext, provision, update, toClear) ->
attributes = {}
timestamp = sessionContext.timestamp + sessionContext.iteration
timestamp += 1 if provision[2]?
if update.writable?
attributes.writable = [timestamp, +update.writable]
else if provision[1]?.writable? or provision[2]?.writable?
throw new Error('Virtual parameter must provide declared attributes')
if update.value?
attributes.value = [timestamp, device.sanitizeParameterValue(update.value)]
else if provision[1]?.value? or provision[2]?.value?
throw new Error('Virtual parameter must provide declared attributes')
return device.set(sessionContext.deviceData, ['VirtualParameters', provision[0]], timestamp, attributes, toClear)
rpcResponse = (sessionContext, id, rpcRes, callback) ->
if id != generateRpcId(sessionContext)
return callback(new Error('Request ID not recognized'))
++ sessionContext.rpcCount
rpcReq = sessionContext.rpcRequest
if not rpcReq.next?
sessionContext.rpcRequest = null
else if rpcReq.next == 'getInstanceKeys'
instanceNumber = rpcRes.instanceNumber
parameterNames = []
instanceValues = {}
for k, v of rpcReq.instanceValues
n = "#{rpcReq.objectName}#{rpcRes.instanceNumber}.#{k}"
parameterNames.push(n)
instanceValues[n] = v
if parameterNames.length == 0
sessionContext.rpcRequest = null
else
sessionContext.rpcRequest = {
name: 'GetParameterValues'
parameterNames: parameterNames
next: 'setInstanceKeys'
instanceValues: instanceValues
}
else if rpcReq.next == 'setInstanceKeys'
parameterList = []
for p in rpcRes.parameterList
if p[1] != rpcReq.instanceValues[p[0]]
parameterList.push(
[p[0]].concat(device.sanitizeParameterValue([rpcReq.instanceValues[p[0]], p[2]])))
if not parameterList.length
sessionContext.rpcRequest = null
else
sessionContext.rpcRequest = {
name: 'SetParameterValues'
parameterList: parameterList
}
timestamp = sessionContext.timestamp + sessionContext.iteration
revision = sessionContext.revisions[sessionContext.revisions.length - 1] + 1
sessionContext.deviceData.timestamps.revision = revision
sessionContext.deviceData.attributes.revision = revision
toClear = null
switch rpcRes.name
when 'GetParameterValuesResponse'
return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'GetParameterValues'
for p in rpcRes.parameterList
toClear = device.set(sessionContext.deviceData, common.parsePath(p[0]), timestamp,
{object: [timestamp, 0], value: [timestamp, p.slice(1)]}, toClear)
when 'GetParameterNamesResponse'
return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'GetParameterNames'
if rpcReq.parameterPath.endsWith('.')
root = common.parsePath(rpcReq.parameterPath.slice(0, -1))
else
root = common.parsePath(rpcReq.parameterPath)
params = []
params.push([root.concat('*'), timestamp])
# Some clients don't report all ancestors explicitly
missing = {}
for p in rpcRes.parameterList
i = p[0].length - 1
while (i = p[0].lastIndexOf('.', i - 1)) > rpcReq.parameterPath.length
missing[p[0].slice(0, i)] |= 0
if p[0].endsWith('.')
missing[p[0][0...-1]] |= 1
path = common.parsePath(p[0][0...-1])
if not rpcReq.nextLevel
params.push([path.concat('*'), timestamp])
params.push([path, timestamp,
{object: [timestamp, 1], writable: [timestamp, if p[1] then 1 else 0]}])
else
missing[p[0]] |= 1
params.push([common.parsePath(p[0]), timestamp,
{object: [timestamp, 0], writable: [timestamp, if p[1] then 1 else 0]}])
for k, v of missing when v == 0
# TODO consider showing a warning
path = common.parsePath(k)
params.push([path, timestamp, {object: [timestamp, 1], writable: [timestamp, 0]}])
params.push([path.concat('*'), timestamp])
# Sort such that actual parameters are set before wildcard ones
params.sort((a, b) ->
al = a[0].length
bl = b[0].length
++ bl if b[0][bl - 1] == '*'
++ al if a[0][al - 1] == '*'
return al - bl
)
if rpcReq.nextLevel
loadPath(sessionContext, root, (1 << (root.length + 1)) - 1)
else
loadPath(sessionContext, root, (1 << config.get('MAX_DEPTH', sessionContext.deviceId)) - 1)
loadParameters(sessionContext, (err) ->
return callback(err) if err
if root.length == 0
for n in ['DeviceID', 'Events', 'Tags', 'Reboot', 'FactoryReset', 'VirtualParameters']
if p = sessionContext.deviceData.paths.get([n])
if sessionContext.deviceData.attributes.has(p)
sessionContext.deviceData.timestamps.set(p, timestamp)
for p in params
toClear = device.set(sessionContext.deviceData, p[0], p[1], p[2], toClear)
clear(sessionContext, toClear, callback)
)
return
when 'SetParameterValuesResponse'
return callback(new Error('Response name does not match request name')) if rpcReq.name isnt 'SetParameterValues'
for p in rpcReq.parameterList
toClear = device.set(sessionContext.deviceData, common.parsePath(p[0]), timestamp + 1,
{object: [timestamp + 1, 0], writable: [timestamp + 1, 1], value: [timestamp + 1, p.slice(1)]}, toClear)
when 'AddObjectResponse'
toClear = device.set(sessionContext.deviceData, common.parsePath(rpcReq.objectName + rpcRes.instanceNumber),
timestamp + 1, {object: [timestamp + 1, 1]}, toClear)
when 'DeleteObjectResponse'
toClear = device.set(sessionContext.deviceData, common.parsePath(rpcReq.objectName.slice(0, -1)),
timestamp + 1, null, toClear)
when 'RebootResponse'
toClear = device.set(sessionContext.deviceData, common.parsePath('Reboot'),
timestamp + 1, {value: [timestamp + 1, [timestamp + 1, 'xsd:dateTime']]}, toClear)
when 'FactoryResetResponse'
toClear = device.set(sessionContext.deviceData, common.parsePath('FactoryReset'),
timestamp + 1, {value: [timestamp + 1, [timestamp + 1, 'xsd:dateTime']]}, toClear)
when 'DownloadResponse'
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'Download'],
timestamp + 1, {value: [timestamp + 1, [timestamp + 1, 'xsd:dateTime']]}, toClear)
if rpcRes.status == 0
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'LastDownload'],
timestamp + 1, {value: [timestamp + 1, [timestamp + 1, 'xsd:dateTime']]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'LastFileType'],
timestamp + 1, {value: [timestamp + 1, [rpcReq.fileType, 'xsd:dateTime']]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'LastFileName'],
timestamp + 1, {value: [timestamp + 1, [rpcReq.fileType, 'xsd:dateTime']]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'LastTargetFileName'],
timestamp + 1, {value: [timestamp + 1, [rpcReq.fileType, 'xsd:dateTime']]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'StartTime'],
timestamp + 1, {value: [timestamp + 1, [+rpcRes.startTime, 'xsd:dateTime']]}, toClear)
toClear = device.set(sessionContext.deviceData, ['Downloads', rpcReq.instance, 'CompleteTime'],
timestamp + 1, {value: [timestamp + 1, [+rpcRes.completeTime, 'xsd:dateTime']]}, toClear)
else
operation = {
name: 'Download'
timestamp: sessionContext.timestamp
provisions: sessionContext.provisions
channels: sessionContext.channels
retries: {}
args: {
instance: rpcReq.instance
fileType: rpcReq.fileType
fileName: rpcReq.fileName
targetFileName: rpcReq.targetFileName
}
}
for channel of sessionContext.channels
if sessionContext.retries[channel]?
operation.retries[channel] = sessionContext.retries[channel]
sessionContext.operations[rpcReq.commandKey] = operation
sessionContext.operationsTouched ?= {}
sessionContext.operationsTouched[rpcReq.commandKey] = 1
else
return callback(new Error('Response name not recognized'))
return clear(sessionContext, toClear, callback)
rpcFault = (sessionContext, id, faultResponse, callback) ->
# TODO Consider handling invalid parameter faults by automatically refreshing
# relevant data model portions
fault = {
code: "cwmp.#{faultResponse.detail.faultCode}"
message: faultResponse.detail.faultString
detail: faultResponse.detail
timestamp: sessionContext.timestamp
}
delete sessionContext.syncState
return callback(null, fault)
deserialize = (sessionContextString, callback) ->
localCache.getProvisionsAndVirtualParameters((err, hash, provisions, virtualParameters) ->
return callback(err) if err
sessionContext = JSON.parse(sessionContextString)
if sessionContext.presetsHash? and sessionContext.presetsHash != hash
return callback(new Error('Preset hash mismatch'))
provisionsCache.set(sessionContext, provisions)
virtualParametersCache.set(sessionContext, virtualParameters)
for d in sessionContext.declarations
common.addPathMeta(d[0])
deviceData = initDeviceData()
for r in sessionContext.deviceData
path = deviceData.paths.add(r[0])
if r[1]
deviceData.loaded.set(path, r[1])
if r[2]
deviceData.trackers.set(path, r[2])
if r[3]
deviceData.timestamps.setRevisions(path, r[3])
if r[4]
deviceData.attributes.setRevisions(path, r[4])
sessionContext.deviceData = deviceData
return callback(null, sessionContext)
)
serialize = (sessionContext, callback) ->
deviceData = []
for path in sessionContext.deviceData.paths.find([], false, false, 99)
e = [path]
e[1] = sessionContext.deviceData.loaded.get(path) || 0
e[2] = sessionContext.deviceData.trackers.get(path) || null
e[3] = sessionContext.deviceData.timestamps.getRevisions(path) || null
e[4] = sessionContext.deviceData.attributes.getRevisions(path) || null
deviceData.push(e)
oldDeviceData = sessionContext.deviceData
oldSyncState = sessionContext.syncState
oldToLoad = sessionContext.toLoad
sessionContext.deviceData = deviceData
delete sessionContext.syncState
delete sessionContext.toLoad
sessionContextString = JSON.stringify(sessionContext)
sessionContext.deviceData = oldDeviceData
sessionContext.syncState = oldSyncState
sessionContext.toLoad = oldToLoad
return callback(null, sessionContextString)
exports.init = init
exports.timeoutOperations = timeoutOperations
exports.inform = inform
exports.transferComplete = transferComplete
exports.addProvisions = addProvisions
exports.clearProvisions = clearProvisions
exports.rpcRequest = rpcRequest
exports.rpcResponse = rpcResponse
exports.rpcFault = rpcFault
exports.serialize = serialize
exports.deserialize = deserialize