diff --git a/lib/api-functions.ts b/lib/api-functions.ts index e1a7d68..e7950f8 100644 --- a/lib/api-functions.ts +++ b/lib/api-functions.ts @@ -268,6 +268,19 @@ function sanitizeTask(task): void { ) throw new Error("Invalid 'targetFileName' property"); break; + + case "provisions": + if ( + !Array.isArray(task.provisions) || + !task.provisions.every((arr) => + arr.every( + (s) => + s == null || ["boolean", "number", "string"].includes(typeof s) + ) + ) + ) + throw new Error("Invalid 'provisions' property"); + break; } return task; diff --git a/lib/cwmp.ts b/lib/cwmp.ts index a17dbdb..e6b0de2 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -715,6 +715,13 @@ async function nextRpc(sessionContext: SessionContext): Promise { ["instances", task.objectName, 0], ]); break; + case "provisions": + session.addProvisions( + sessionContext, + `task_${task._id}`, + task.provisions + ); + break; default: throw new Error("Task name not recognized"); } diff --git a/lib/db.ts b/lib/db.ts index d482fc7..7b08073 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -47,6 +47,12 @@ export let tasksCollection: Collection, let clientPromise: Promise; export let client: MongoClient; +function compareAccessLists(list1: string[], list2: string[]): boolean { + if (list1.length !== list2.length) return false; + for (const [i, v] of list1.entries()) if (v !== list2[i]) return false; + return true; +} + export async function connect(): Promise { clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), { useNewUrlParser: true, @@ -119,6 +125,8 @@ export async function fetchDevice( function storeParams(obj, path: string, pathLength: number, ts): void { if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"]; + if (obj["_attributesTimestamp"]) + obj["_attributesTimestamp"] = +obj["_attributesTimestamp"]; const attrs: Attributes = {}; let t = obj["_timestamp"] || 1; @@ -136,6 +144,16 @@ export async function fetchDevice( if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; + if (obj["_notification"] != null) { + attrs.notification = [ + obj["_attributesTimestamp"] || 1, + obj["_notification"], + ]; + } + + if (obj["_accessList"] != null) + attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; + res.push([Path.parse(path.slice(0, -1)), t, attrs]); for (const [k, v] of Object.entries(obj)) { @@ -332,6 +350,12 @@ export async function saveDevice( const object2 = ((diff[2] || {}).object || [])[1]; const writable2 = ((diff[2] || {}).writable || [])[1]; const writable1 = ((diff[1] || {}).writable || [])[1]; + const attributesTimestamp1 = ((diff[1] || {}).notification || [])[0]; + const attributesTimestamp2 = ((diff[2] || {}).notification || [])[0]; + const notification1 = ((diff[1] || {}).notification || [])[1]; + const notification2 = ((diff[2] || {}).notification || [])[1]; + const accessList1 = ((diff[1] || {}).accessList || [])[1]; + const accessList2 = ((diff[2] || {}).accessList || [])[1]; switch (path.segments[0]) { case "Events": @@ -458,6 +482,46 @@ export async function saveDevice( path.length ? path.toString() + "._writable" : "_writable" ] = !!writable2; } + + break; + case "notification": + if ( + !diff[1] || + !diff[1].notification || + notification2 !== notification1 + ) { + update["$set"][ + path.length + ? path.toString() + "._notification" + : "_notification" + ] = notification2; + } + + if (attributesTimestamp2 !== attributesTimestamp1) { + update["$set"][ + path.toString() + "._attributesTimestamp" + ] = new Date(attributesTimestamp2); + } + + break; + case "accessList": + if ( + !diff[1] || + !diff[1].accessList || + !compareAccessLists(accessList2, accessList1) + ) { + update["$set"][ + path.length + ? path.toString() + "._accessList" + : "_accessList" + ] = accessList2; + } + + if (attributesTimestamp2 !== attributesTimestamp1) { + update["$set"][ + path.toString() + "._attributesTimestamp" + ] = new Date(attributesTimestamp2); + } } } } @@ -473,6 +537,12 @@ export async function saveDevice( if (attrName === "value") { update["$unset"][p + "_type"] = 1; update["$unset"][p + "_timestamp"] = 1; + } else if (attrName === "notification") { + if (accessList2 == null) + update["$unset"][`${p}_attributesTimestamp`] = 1; + } else if (attrName === "accessList") { + if (notification2 == null) + update["$unset"][`${p}_attributesTimestamp`] = 1; } } } diff --git a/lib/default-provisions.ts b/lib/default-provisions.ts index 3d35540..04b1d28 100644 --- a/lib/default-provisions.ts +++ b/lib/default-provisions.ts @@ -34,25 +34,45 @@ export function refresh( (provision.length !== 2 || typeof provision[1] !== "string") && (provision.length !== 3 || typeof provision[1] !== "string" || - typeof provision[2] !== "number") + typeof provision[2] !== "number") && + (provision.length < 4 || + typeof provision[1] !== "string" || + typeof provision[2] !== "number" || + typeof provision[3] !== "boolean") ) throw new Error("Invalid arguments"); - const segments = Path.parse(provision[1]).segments.slice(); - const l = segments.length; - segments.length = MAX_DEPTH; - segments.fill("*", l); - const path = Path.parse(segments.join(".")); const every = 1000 * ((provision[2] as number) || 1); const offset = scheduling.variance(sessionContext.deviceId, every); const t = scheduling.interval(sessionContext.timestamp, every, offset); - for (let i = l; i < path.length; ++i) { + let attrGet; + let refreshChildren; + if (provision[3] == null) { + refreshChildren = true; + attrGet = { object: 1, writable: 1, value: t }; + } else { + attrGet = {}; + refreshChildren = !!provision[3]; + for (const a of provision.slice(4)) attrGet[a as string] = t; + } + + let path = Path.parse(provision[1]); + let l = path.length; + if (refreshChildren) { + const segments = path.segments.slice(); + l = segments.length; + segments.length = MAX_DEPTH; + segments.fill("*", l); + path = Path.parse(segments.join(".")); + } + + for (let i = l; i <= path.length; ++i) { declarations.push({ path: path.slice(0, i), pathGet: t, pathSet: null, - attrGet: { object: 1, writable: 1, value: t }, + attrGet: attrGet, attrSet: null, defer: true, }); @@ -66,15 +86,38 @@ export function value( provision: (string | number | boolean)[], declarations: Declaration[] ): boolean { - if (provision.length !== 3 || typeof provision[1] !== "string") + if ( + provision.length < 3 || + provision.length > 4 || + typeof provision[1] !== "string" + ) throw new Error("Invalid arguments"); + let attr: string, val: any; + + if (provision.length === 3) { + attr = "value"; + val = provision[2]; + } else { + attr = (provision[2] as string) || ""; + val = provision[3]; + } + + if (attr === "accessList") { + val = (val || "") + .split(",") + .map((s) => s.trim()) + .filter((s) => !!s); + } else if (attr === "value") { + val = [val]; + } + declarations.push({ path: Path.parse(provision[1]), pathGet: 1, pathSet: null, - attrGet: { value: 1 }, - attrSet: { value: [provision[2]] }, + attrGet: { [attr]: 1 }, + attrSet: { [attr]: val }, defer: true, }); diff --git a/lib/device.ts b/lib/device.ts index 5089cd9..ee94642 100644 --- a/lib/device.ts +++ b/lib/device.ts @@ -30,6 +30,8 @@ const CHANGE_FLAGS = { object: 2, writable: 4, value: 8, + notification: 16, + accessList: 32, }; function parseBool(v): boolean { diff --git a/lib/mongodb-functions.ts b/lib/mongodb-functions.ts index c98dff1..b922abe 100644 --- a/lib/mongodb-functions.ts +++ b/lib/mongodb-functions.ts @@ -304,19 +304,30 @@ function parseDate(d: Date): number | string { return isNaN(n) ? "" + d : n; } +interface FlatAttributes { + object?: boolean; + objectTimestamp?: number; + writable?: boolean; + writableTimestamp?: number; + value?: [string | number | boolean, string]; + valueTimestamp?: number; + notification?: number; + notificationTimestamp?: number; + accessList?: string[]; + accessListTimestamp?: number; +} + interface FlatDevice { - [param: string]: { - object: boolean; - objectTimestamp: number; - writable: boolean; - writableTimestamp: number; - value: [string | number | boolean, string]; - valueTimestamp: number; - }; + [param: string]: FlatAttributes; } export function flattenDevice(device: Record): FlatDevice { - function recursive(input, root, output, timestamp): void { + function recursive( + input, + root: string, + output: FlatDevice, + timestamp: number + ): void { for (const [name, tree] of Object.entries(input)) { if (!root) { if (name === "_lastInform") { @@ -357,7 +368,7 @@ export function flattenDevice(device: Record): FlatDevice { }; } else if (name === "_id") { output["DeviceID.ID"] = { - value: [tree, "xsd:string"], + value: [tree as string, "xsd:string"], valueTimestamp: timestamp, writable: false, writableTimestamp: timestamp, @@ -426,34 +437,44 @@ export function flattenDevice(device: Record): FlatDevice { else if (+input["_timestamp"] > timestamp) childrenTimestamp = +input["_timestamp"]; - const attrs = {}; + const attrs: FlatAttributes = {}; if (tree["_value"] != null) { - attrs["value"] = [ + attrs.value = [ tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"], tree["_type"], ]; - attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp); - attrs["object"] = false; - attrs["objectTimestamp"] = childrenTimestamp; + attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp); + attrs.object = false; + attrs.objectTimestamp = childrenTimestamp; } else if (tree["_object"] != null) { - attrs["object"] = tree["_object"]; - attrs["objectTimestamp"] = childrenTimestamp; + attrs.object = tree["_object"]; + attrs.objectTimestamp = childrenTimestamp; } if (tree["_writable"] != null) { - attrs["writable"] = tree["_writable"]; - attrs["writableTimestamp"] = childrenTimestamp; + attrs.writable = tree["_writable"]; + attrs.writableTimestamp = childrenTimestamp; + } + + if (tree["_notification"] != null) { + attrs.notification = tree["_notification"]; + attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1; + } + + if (tree["_accessList"] != null) { + attrs.accessList = tree["_accessList"]; + attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1; } const r = root ? `${root}.${name}` : name; output[r] = attrs; - if (attrs["object"] || tree["object"] == null) + if (attrs.object || tree["object"] == null) recursive(tree, r, output, childrenTimestamp); } } - const newDevice = {}; + const newDevice: FlatDevice = {}; const timestamp = new Date((device["_lastInform"] as Date) || 1).getTime(); recursive(device, "", newDevice, timestamp); return newDevice; diff --git a/lib/session.ts b/lib/session.ts index e9ba5f6..79881b5 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -770,8 +770,11 @@ function runDeclarations( object: new Set(), writable: new Set(), value: new Set(), + notification: new Set(), + accessList: new Set(), }, spv: new Map(), + spa: new Map(), gpn: new Set(), gpnPatterns: new Map(), tags: new Map(), @@ -1508,6 +1511,8 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { syncState.refreshAttributes.value.has(p) || syncState.refreshAttributes.object.has(p) || syncState.refreshAttributes.writable.has(p) || + syncState.refreshAttributes.notification.has(p) || + syncState.refreshAttributes.accessList.has(p) || syncState.gpn.has(p) ) { found = true; @@ -1536,7 +1541,9 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { syncState.refreshAttributes.value.has(p) || (p.length > path.length && (syncState.refreshAttributes.object.has(p) || - syncState.refreshAttributes.writable.has(p))) + syncState.refreshAttributes.writable.has(p) || + syncState.refreshAttributes.notification.has(p) || + syncState.refreshAttributes.accessList.has(p))) ) { found = true; break; @@ -1648,9 +1655,59 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { }; } } + + if ( + syncState.refreshAttributes.notification.size || + syncState.refreshAttributes.accessList.size + ) { + const GPV_BATCH_SIZE = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.gpvBatchSize", + {}, + sessionContext.timestamp, + (e) => configContextCallback(sessionContext, e) + ); + + const parameterNames: string[] = []; + for (const path of syncState.refreshAttributes.notification) { + syncState.refreshAttributes.notification.delete(path); + syncState.refreshAttributes.accessList.delete(path); + // Need to check in case param is deleted + const attrs = sessionContext.deviceData.attributes.get(path); + if (attrs) { + parameterNames.push(path.toString()); + if (parameterNames.length >= GPV_BATCH_SIZE) break; + } + } + + if (parameterNames.length < GPV_BATCH_SIZE) { + for (const path of syncState.refreshAttributes.accessList) { + syncState.refreshAttributes.accessList.delete(path); + const attrs = sessionContext.deviceData.attributes.get(path); + if (attrs) { + parameterNames.push(path.toString()); + if (parameterNames.length >= GPV_BATCH_SIZE) break; + } + } + } + + if (parameterNames.length) { + return { + name: "GetParameterAttributes", + parameterNames: parameterNames, + }; + } + } + return null; } +function compareAccessLists(list1: string[], list2: string[]): boolean { + if (list1.length !== list2.length) return false; + for (const [i, v] of list1.entries()) if (v !== list2[i]) return false; + return true; +} + function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { const syncState = sessionContext.syncState; if (!syncState) return null; @@ -1709,7 +1766,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { (e) => configContextCallback(sessionContext, e) ); - const parameterValues: [Path, string | number | boolean, string][] = []; + const parameterValues: [string, string | number | boolean, string][] = []; for (const [k, v] of syncState.spv) { syncState.spv.delete(k); const attrs = sessionContext.deviceData.attributes.get(k); @@ -1728,7 +1785,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { val[0] -= val[0] % 1000; if (val[0] !== curVal[0] || val[1] !== curVal[1]) - parameterValues.push([k, val[0], val[1]]); + parameterValues.push([k.toString(), val[0], val[1]]); if (parameterValues.length >= GPV_BATCH_SIZE) break; } @@ -1737,19 +1794,44 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest { if (parameterValues.length) { return { name: "SetParameterValues", - parameterList: parameterValues.map( - (p) => - [p[0].toString(), p[1], p[2]] as [ - string, - string | number | boolean, - string - ] - ), + parameterList: parameterValues, DATETIME_MILLISECONDS: DATETIME_MILLISECONDS, BOOLEAN_LITERAL: BOOLEAN_LITERAL, }; } + // Set attributes + const parameterAttributes: [string, number, string[]][] = []; + for (const [k, v] of syncState.spa) { + syncState.spa.delete(k); + const attrs = sessionContext.deviceData.attributes.get(k); + + if ( + v.notification != null && + (!attrs.notification || v.notification === attrs.notification[1]) + ) + v.notification = null; + + if ( + v.accessList != null && + (!attrs.accessList || + compareAccessLists(v.accessList, attrs.accessList[1])) + ) + v.accessList = null; + + if (v.notification != null || v.accessList != null) + parameterAttributes.push([k.toString(), v.notification, v.accessList]); + + if (parameterAttributes.length >= GPV_BATCH_SIZE) break; + } + + if (parameterAttributes.length) { + return { + name: "SetParameterAttributes", + parameterList: parameterAttributes, + }; + } + // Downloads for (const [p, t] of syncState.downloadsDownload) { const attrs = deviceData.attributes.get(p); @@ -2167,8 +2249,34 @@ function processDeclarations( } } } - if (declareAttributeValues && declareAttributeValues.value != null) - syncState.spv.set(currentPath, declareAttributeValues.value); + if (declareAttributeValues) { + if (declareAttributeValues.value != null) + syncState.spv.set(currentPath, declareAttributeValues.value); + + if (declareAttributeValues.notification != null) { + const spa = syncState.spa.get(currentPath); + if (spa) { + spa.notification = declareAttributeValues.notification; + } else { + syncState.spa.set(currentPath, { + notification: declareAttributeValues.notification, + accessList: null, + }); + } + } + + if (declareAttributeValues.accessList != null) { + const spa = syncState.spa.get(currentPath); + if (spa) { + spa.accessList = declareAttributeValues.accessList; + } else { + syncState.spa.set(currentPath, { + notification: null, + accessList: declareAttributeValues.accessList, + }); + } + } + } } } @@ -2305,7 +2413,7 @@ export async function rpcResponse( if (p[1] !== req.instanceValues[p[0]]) { const v = device.sanitizeParameterValue([ req.instanceValues[p[0]], - p[2], + p[2] as string, ]); parameterList.push([p[0], v[0], v[1]]); } @@ -2371,6 +2479,24 @@ export async function rpcResponse( toClear ); } + } else if (rpcRes.name === "GetParameterAttributesResponse") { + const req = rpcReq as GetAcsRequest; + const res = rpcRes as CpeGetResponse; + if (req.name !== "GetParameterAttributes") + throw new Error("Response name does not match request name"); + + for (const p of res.parameterList) { + toClear = device.set( + sessionContext.deviceData, + Path.parse(p[0]), + timestamp, + { + notification: [timestamp, p[1] as number], + accessList: [timestamp, p[2] as string[]], + }, + toClear + ); + } } else if (rpcRes.name === "GetParameterNamesResponse") { const req = rpcReq as GetAcsRequest; const res = rpcRes as CpeGetResponse; @@ -2485,6 +2611,37 @@ export async function rpcResponse( toClear ); } + } else if (rpcRes.name === "SetParameterAttributesResponse") { + const req = rpcReq as SetAcsRequest; + if (req.name !== "SetParameterAttributes") + throw new Error("Response name does not match request name"); + + for (const p of req.parameterList) { + let attrs; + + if (p[1] != null && p[2] != null) { + attrs = { + notification: [timestamp + 1, p[1]], + accessList: [timestamp + 1, p[2]], + }; + } else if (p[1] != null) { + attrs = { + notification: [timestamp + 1, p[1]], + }; + } else if (p[2] != null) { + attrs = { + accessList: [timestamp + 1, p[2]], + }; + } + + toClear = device.set( + sessionContext.deviceData, + Path.parse(p[0]), + timestamp + 1, + attrs, + toClear + ); + } } else if (rpcRes.name === "AddObjectResponse") { const req = rpcReq as SetAcsRequest; const res = rpcRes as CpeSetResponse; @@ -2628,7 +2785,7 @@ export async function rpcFault( // Recover from invalid parameter name faults if (faultResponse.detail.faultCode === "9005") { - const timestamp = sessionContext.timestamp + sessionContext.iteration; + const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const revision = (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; sessionContext.deviceData.timestamps.revision = revision; @@ -2646,13 +2803,25 @@ export async function rpcFault( (p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear ); } else if (rpcReq.name === "SetParameterValues") { - toClear = rpcReq.parameterList.map( + toClear = (rpcReq.parameterList as [ + string, + string | number | boolean, + string + ][]).map( (p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear ); } else if (rpcReq.name === "AddObject") { toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } else if (rpcReq.name === "DeleteObject") { toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; + } else if (rpcReq.name === "GetParameterAttributes") { + toClear = rpcReq.parameterNames.map( + (p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear + ); + } else if (rpcReq.name === "SetParameterAttributes") { + toClear = (rpcReq.parameterList as [string, number, string[]][]).map( + (p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear + ); } if (toClear) { diff --git a/lib/soap.ts b/lib/soap.ts index 14ccddc..14907cc 100644 --- a/lib/soap.ts +++ b/lib/soap.ts @@ -191,6 +191,45 @@ function parameterValueList( }); } +function parameterAttributeList(xml: Element): [string, number, string[]][] { + return xml.children + .filter((e) => e.localName === "ParameterAttributeStruct") + .map<[string, number, string[]]>((e) => { + let notificationElement: Element, + accessListElement: Element, + param: string; + for (const c of e.children) { + switch (c.localName) { + case "Name": + param = c.text; + break; + case "Notification": + notificationElement = c; + break; + case "AccessList": + accessListElement = c; + break; + } + } + + const notification = decodeEntities(notificationElement.text); + const notif = +notification; + + if (notif !== parseInt(notification)) { + warnings.push({ + message: "Invalid notification attribute", + parameter: param, + }); + } + + const accessList = accessListElement.children + .filter((c) => c.localName === "string") + .map((c) => decodeEntities(c.text)); + + return [param, notif, accessList]; + }); +} + function GetParameterNames(methodRequest): string { return `${ methodRequest.parameterPath @@ -223,6 +262,23 @@ function GetParameterValuesResponse(xml: Element): CpeGetResponse { }; } +function GetParameterAttributes(methodRequest): string { + return `${methodRequest.parameterNames + .map((p) => `${p}`) + .join("")}`; +} + +function GetParameterAttributesResponse(xml: Element): CpeGetResponse { + return { + name: "GetParameterAttributesResponse", + parameterList: parameterAttributeList( + xml.children.find((n) => n.localName === "ParameterList") + ), + }; +} + function SetParameterValues(methodRequest): string { const params = methodRequest.parameterList.map((p) => { let val = p[1]; @@ -252,6 +308,36 @@ function SetParameterValuesResponse(xml: Element): CpeSetResponse { }; } +function SetParameterAttributes(methodRequest): string { + const params = methodRequest.parameterList.map((p) => { + return `${ + p[0] + }${ + p[1] == null ? "false" : "true" + }${ + p[1] == null ? "" : p[1] + }${ + p[2] == null ? "false" : "true" + }${ + p[2] == null + ? "" + : p[2].map((s) => `${encodeEntities(s)}`).join("") + }`; + }); + + return `${params.join("")}`; +} + +function SetParameterAttributesResponse(): CpeSetResponse { + return { + name: "SetParameterAttributesResponse", + }; +} + function AddObject(methodRequest): string { return `${ methodRequest.objectName @@ -622,9 +708,15 @@ export function request( case "GetParameterValuesResponse": rpc.cpeResponse = GetParameterValuesResponse(methodElement); break; + case "GetParameterAttributesResponse": + rpc.cpeResponse = GetParameterAttributesResponse(methodElement); + break; case "SetParameterValuesResponse": rpc.cpeResponse = SetParameterValuesResponse(methodElement); break; + case "SetParameterAttributesResponse": + rpc.cpeResponse = SetParameterAttributesResponse(); + break; case "AddObjectResponse": rpc.cpeResponse = AddObjectResponse(methodElement); break; @@ -707,9 +799,15 @@ export function response(rpc: { case "GetParameterValues": body = GetParameterValues(rpc.acsRequest); break; + case "GetParameterAttributes": + body = GetParameterAttributes(rpc.acsRequest); + break; case "SetParameterValues": body = SetParameterValues(rpc.acsRequest); break; + case "SetParameterAttributes": + body = SetParameterAttributes(rpc.acsRequest); + break; case "AddObject": body = AddObject(rpc.acsRequest); break; diff --git a/lib/types.ts b/lib/types.ts index 659444f..2305eb6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -52,18 +52,24 @@ export interface Attributes { object?: [number, 1 | 0]; writable?: [number, 1 | 0]; value?: [number, [string | number | boolean, string]]; + notification?: [number, number]; + accessList?: [number, string[]]; } export interface AttributeTimestamps { object?: number; writable?: number; value?: number; + notification?: number; + accessList?: number; } export interface AttributeValues { object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; } export interface DeviceData { @@ -76,12 +82,21 @@ export interface DeviceData { export type VirtualParameterDeclaration = [ Path, - { path?: number; object?: number; writable?: number; value?: number }?, + { + path?: number; + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }?, { path?: [number, number]; object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; }? ]; @@ -91,8 +106,11 @@ export interface SyncState { object: Set; writable: Set; value: Set; + notification: Set; + accessList: Set; }; spv: Map; + spa: Map; gpn: Set; gpnPatterns: Map; tags: Map; @@ -161,6 +179,7 @@ export interface Task { fileName?: string; targetFileName?: string; expiry?: number; + provisions?: (string | number | boolean)[][]; } export interface Operation { @@ -183,7 +202,7 @@ export interface AcsRequest { } export interface GetAcsRequest extends AcsRequest { - name: "GetParameterNames" | "GetParameterValues"; + name: "GetParameterNames" | "GetParameterValues" | "GetParameterAttributes"; objectName?: string; parameterNames?: string[]; parameterPath?: string; @@ -194,12 +213,15 @@ export interface GetAcsRequest extends AcsRequest { export interface SetAcsRequest extends AcsRequest { name: | "SetParameterValues" + | "SetParameterAttributes" | "AddObject" | "DeleteObject" | "FactoryReset" | "Reboot" | "Download"; - parameterList?: [string, string | number | boolean, string][]; + parameterList?: + | [string, string | number | boolean, string][] + | [string, number, string[]][]; instanceValues?: { [param: string]: string | number | boolean }; objectName?: string; DATETIME_MILLISECONDS?: boolean; @@ -236,15 +258,20 @@ export interface CpeFault { } export interface CpeGetResponse extends CpeResponse { - name: "GetParameterNamesResponse" | "GetParameterValuesResponse"; + name: + | "GetParameterNamesResponse" + | "GetParameterValuesResponse" + | "GetParameterAttributesResponse"; parameterList?: | [string, boolean][] - | [string, string | number | boolean, string][]; + | [string, string | number | boolean, string][] + | [string, number, string[]][]; } export interface CpeSetResponse extends CpeResponse { name: | "SetParameterValuesResponse" + | "SetParameterAttributesResponse" | "AddObjectResponse" | "DeleteObjectResponse" | "RebootResponse" @@ -301,11 +328,19 @@ export interface Declaration { path: Path; pathGet: number; pathSet?: number | [number, number]; - attrGet?: { object?: number; writable?: number; value?: number }; + attrGet?: { + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }; attrSet?: { object?: boolean; writable?: boolean; value?: [string | number | boolean, string?]; + notification?: number; + accessList?: string[]; }; defer: boolean; } @@ -313,7 +348,13 @@ export interface Declaration { export type Clear = [ Path, number, - { object?: number; writable?: number; value?: number }?, + { + object?: number; + writable?: number; + value?: number; + notification?: number; + accessList?: number; + }?, number? ]; diff --git a/lib/ui/api.ts b/lib/ui/api.ts index fcf09e2..22f6f19 100644 --- a/lib/ui/api.ts +++ b/lib/ui/api.ts @@ -100,7 +100,7 @@ router.get(`/devices/:id.csv`, async (ctx) => { ctx.body = new stream.PassThrough(); ctx.body.write( - "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n" + "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n" ); for (const k of Object.keys(res[0]).sort()) { @@ -114,6 +114,10 @@ router.get(`/devices/:id.csv`, async (ctx) => { p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "", p.value != null ? p.value[1] : "", p.valueTimestamp, + p.notification, + p.notificationTimestamp, + p.accessList ? p.accessList.join(", ") : "", + p.accessListTimestamp, ]; ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n"); }