UNPKG

s7webserverapi

Version:

Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs

238 lines (237 loc) 8.96 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CacheStructure = void 0; /** * Helper class that holds the cache-structure for the PLC-Connector. * Its used to easily access and write values in the cache given a flattened key that fits to the template T * * @export * @class CacheStructure * @typedef {CacheStructure} * @template T - The type of the cacheObject that is used to store the values */ class CacheStructure { /** * Holds the actual values of the cache * * @public * @type {{}} */ cacheObject = {}; /** * Parses a flattened key into an array of key-elements. A number is later interpreted as an array-index. A string is interpreted as an object-key * * @public * @param {(FlattenKeys<T> | string)} flattenedKey * @returns {(string | number)[]} */ parseFlattenedKey(flattenedKey) { return flattenedKey.split('.').map(key => { // Check if the key is a number and convert it if so return isNaN(Number(key)) ? key : Number(key); }); } constructor(initObject) { this.cacheObject = initObject ?? {}; } /** * Static helper function that gets the next reference from a given reference and a new key. * * @public * @static * @param {*} ref * @param {string} newKey * @returns {any} */ static getNextReference(ref, newKey) { if (newKey == '' || ref == undefined) { return ref; } if (!Number.isNaN(Number(newKey))) { if (Array.isArray(ref)) { ref = ref[Number(newKey)]; return ref; } else { throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given reference, ${newKey} is a number, but the given reference is not an array`); } } if (typeof ref === 'object' && !Array.isArray(ref)) { if (!(newKey in ref)) { throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given reference`); } ref = ref[newKey]; return ref; } throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given ref, ${newKey} is a string, but the given reference is not an object`); } /** * Checks if an entry with the given flattened key exists in the cache * * @public * @param {FlattenKeys<T>} flattenedKey * @returns {boolean} */ entryExists(flattenedKey) { const keys = this.parseFlattenedKey(flattenedKey); // eslint-disable-next-line @typescript-eslint/no-explicit-any let current = this.cacheObject; for (const key of keys) { if (current == null || !(key in current)) { return false; } if (Array.isArray(current) && typeof key === 'number') { current = current[key]; } else if (typeof key === 'string' && typeof current === 'object' && !Array.isArray(current)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any current = current[key]; } } return true; } /** * Gets a copy of a value in the cache. Useful if the value is an object and we need to change the value somewhere else without chaning the original value in the cache * * @public * @param {FlattenKeys<T>} flattenedKey * @returns {*} */ getCopy(flattenedKey) { const ref = this.getReference(flattenedKey); if (ref == undefined) { return undefined; } return this.deepClone(ref); } /** * Recursive function to deeply clone an object. Since we only have a simple structure that is deeply nested this function should be enough. * @param obj Object to clone * @returns */ deepClone(obj) { if (obj == undefined || typeof obj !== 'object') { return obj; } const clone = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.hasOwn(obj, key)) { clone[key] = this.deepClone(obj[key]); } } return clone; } /** * Gets a reference to a value in the cache. Useful if the value is an object and we need to change the value somewhere else and want to change the original value in the cache * * @public * @param {FlattenKeys<T>} flattenedKey * @returns {any} */ getReference(flattenedKey) { const keys = this.parseFlattenedKey(flattenedKey); let current = this.cacheObject; for (const key of keys) { if (Array.isArray(current) && typeof key === 'number') { current = current[key]; } else if (typeof key === 'string' && typeof current === 'object') { // eslint-disable-next-line @typescript-eslint/no-explicit-any current = current[key]; } else { return undefined; } } return current; } /** * Writes a value to the cache given a flattened key. It also builds up the structure if it does not exist yet. * E.g. if the cache is completley empty {} and we call this function with ("test.someArray", [0,1,2]) the resulting cache looks liek this at the end: * { * test: { * someArray: [0,1,2] * } * } * * @public * @param {FlattenKeys<T>} flattenedKey * @param {*} value */ writeEntry(flattenedKey, value) { const keys = this.parseFlattenedKey(flattenedKey); let current = this.checkAndFillCacheStructure(flattenedKey); if (Array.isArray(current) && typeof keys[keys.length - 1] === 'number') { current[keys[keys.length - 1]] = value; } else if (typeof keys[keys.length - 1] === 'string' && typeof current === 'object') { current = current; current[keys[keys.length - 1]] = value; } } /** * Checks if the key is correct and fills up the cache structure up to the second last key. this is used before we write a value to the cache. * @param flattenedKey * @returns the reference to the second last key in the cache structure */ checkAndFillCacheStructure(flattenedKey) { const keys = this.parseFlattenedKey(flattenedKey); let current = this.cacheObject; keys.slice(0, -1).forEach((key, index) => { current = this.ensureKeyExists(current, key, keys[index + 1]); }); return current; } /** * * @param current Reference to the current level in the cache structure * @param key The key that should be checked, relative to the current level * @param nextKey The next key that should be checked. Given this key we can determine if we need to insert an array or an object * @returns the reference to the next level in the cache structure, given the key. */ ensureKeyExists(current, key, nextKey) { if (!(key in current)) { current[key] = this.initializeNextLevel(current, key, nextKey); } return current[key]; } /** * * @param current Reference to the current level in the cache structure * @param key The key that should be initialized * @param nextKey The next key that should be initialized. Given this next key, we can determine if we need to insert an array or an object * @returns the initialized next level in the cache structure */ initializeNextLevel(current, key, nextKey) { const returnValue = this.determineNewLevelType(nextKey); if (Array.isArray(current) && typeof key === 'number') { return returnValue; } if (typeof key === 'string' && typeof current === 'object') { return returnValue; } throw new Error('Error while filling in the cache structure, key is not a valid key for current object. Key: ' + key + ', Current: ' + current); } /** * Determines if the next level in the cache structure should be an array or an object * Placeholder if in the future we want to use a different structure for the cache * @param nextKey * @returns */ determineNewLevelType(nextKey) { return typeof nextKey === 'number' ? [] : {}; } hmiKeyLoaded(key) { if (Array.isArray(key)) { for (const singleKey of key) { if (!this.entryExists(singleKey)) { return false; } } return true; } return this.entryExists(key); } } exports.CacheStructure = CacheStructure;