UNPKG

iobroker.frigate

Version:
569 lines 27.5 kB
export default class Json2iob { adapter; alreadyCreatedObjects; objectTypes; forbiddenCharsRegex; constructor(adapter) { if (!adapter) { throw new Error('ioBroker Adapter is not defined!'); } this.adapter = adapter; this.alreadyCreatedObjects = {}; this.objectTypes = {}; this.forbiddenCharsRegex = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu; if (this.adapter?.FORBIDDEN_CHARS) { this.forbiddenCharsRegex = this.adapter.FORBIDDEN_CHARS; } } /** * Parses the given element and creates states in the adapter based on the element's structure. * * @param path - The ioBroker object path which the element should be saved to. * @param element - The element to be parsed. * @param options - The parsing options. * @param options.write - Activate write for all states. * @param options.forceIndex - Instead of trying to find names for array entries, use the index as the name. * @param options.disablePadIndex - Disables padding of array index numbers if forceIndex = true * @param options.zeroBasedArrayIndex - Start array index from 0 if forceIndex = true * @param options.channelName - Set name of the root channel. * @param options.preferredArrayName - Set key to use this as an array entry name. * @param options.preferredArrayDesc - Set key to use this as an array entry description. * @param options.autoCast - Make JSON.parse to parse numbers correctly. * @param options.descriptions - Object of names for state keys. * @param options.states - Object of states to create for an id, new entries via JSON will be added automatically to the states. * @param options.units - Object of units to create for an id * @param options.parseBase64 - Parse base64 encoded strings to utf8. * @param options.parseBase64byIds - Array of ids to parse base64 encoded strings to utf8. * @param options.parseBase64byToHex - Array of ids to parse base64 encoded strings to utf8. * @param options.deleteBeforeUpdate - Delete channel before update. * @param options.removePasswords - Remove password from log. * @param options.excludeStateWithEnding - Array of strings to exclude states with this ending. * @param options.makeStateWritableWithEnding - Array of strings to make states with this ending writable. * @param options.dontSaveCreatedObjects - Create objects but do not save them to alreadyCreatedObjects. * @returns A promise that resolves when the parsing is complete. */ async parse(path, element, options = { write: false }) { try { if (element === null || element === undefined) { this.adapter.log.debug(`Cannot extract empty: ${path}`); return; } if ((options.parseBase64 && this._isBase64(element)) || options.parseBase64byIds?.includes(path) || options.parseBase64byIdsToHex?.includes(path)) { try { let value = Buffer.from(element, 'base64').toString('utf8'); if (options.parseBase64byIdsToHex?.includes(path)) { value = Buffer.from(element, 'base64').toString('hex'); } if (this._isJsonString(element)) { value = JSON.parse(element); } element = value; } catch (error) { this.adapter.log.warn(`Cannot parse base64 for ${path}: ${error}`); } } const objectKeys = Object.keys(element); options.write = options.write ?? false; path = path.toString().replace(this.forbiddenCharsRegex, '_'); if (typeof element === 'string' || typeof element === 'number' || typeof element === 'boolean') { // remove ending . from a path if (path.endsWith('.')) { path = path.slice(0, -1); } const lastPathElement = path.split('.').pop() || ''; if (options.excludeStateWithEnding && lastPathElement) { for (const excludeEnding of options.excludeStateWithEnding) { if (lastPathElement.endsWith(excludeEnding)) { this.adapter.log.debug(`skip state with ending : ${path}`); return; } } } if (options.makeStateWritableWithEnding && lastPathElement) { for (const writingEnding of options.makeStateWritableWithEnding) { if (lastPathElement.toLowerCase().endsWith(writingEnding)) { this.adapter.log.debug(`make state with ending writable : ${path}`); options.write = true; } } } if (!this.alreadyCreatedObjects[path] || this.objectTypes[path] !== typeof element) { let type = typeof element; if (this.objectTypes[path] && this.objectTypes[path] !== typeof element) { type = 'mixed'; this.adapter.log.debug(`Type changed for ${path} from ${this.objectTypes[path]} to ${type}`); } let states; if (options.states?.[path] && typeof element !== 'boolean') { states = options.states[path]; states[element] ||= element.toString(); } const common = { name: lastPathElement, role: this._getRole(element, options.write || false), type, write: options.write ?? true, read: true, states, }; if (options.units?.[path]) { common.unit = options.units[path]; } await this._createState(path, common, options); } await this.adapter.setStateAsync(path, element, true); return; } if (options.removePasswords && path.toString().toLowerCase().includes('password')) { this.adapter.log.debug(`skip password : ${path}`); return; } if (!this.alreadyCreatedObjects[path] || options.deleteBeforeUpdate) { if (options.excludeStateWithEnding) { for (const excludeEnding of options.excludeStateWithEnding) { if (path.endsWith(excludeEnding)) { this.adapter.log.debug(`skip state with ending : ${path}`); return; } } } if (options.makeStateWritableWithEnding) { for (const writingEnding of options.makeStateWritableWithEnding) { if (path.toLowerCase().endsWith(writingEnding)) { this.adapter.log.debug(`make state with ending writable : ${path}`); options.write = true; } } } if (options.deleteBeforeUpdate) { this.adapter.log.debug(`Deleting ${path} before update`); for (const key in this.alreadyCreatedObjects) { if (key.startsWith(path)) { delete this.alreadyCreatedObjects[key]; } } await this.adapter.delObjectAsync(path, { recursive: true }); } let name = options.channelName || ''; if (options.preferredArrayDesc && element[options.preferredArrayDesc]) { name = element[options.preferredArrayDesc]; } await this.adapter .extendObjectAsync(path, { type: 'channel', common: { name, }, native: {}, }) .then(() => { if (!options.dontSaveCreatedObjects) { this.alreadyCreatedObjects[path] = true; } options.channelName = undefined; options.deleteBeforeUpdate = undefined; }) .catch((error) => this.adapter.log.error(error)); } if (Array.isArray(element)) { await this._extractArray(element, '', path, options); return; } for (const key of objectKeys) { if (key.toLowerCase().includes('password') && options.removePasswords) { this.adapter.log.debug(`skip password : ${path}.${key}`); return; } if (typeof element[key] === 'function') { this.adapter.log.debug(`Skip function: ${path}.${key}`); continue; } if (element[key] == null) { element[key] = ''; } if (this._isJsonString(element[key]) && options.autoCast) { element[key] = JSON.parse(element[key]); } if ((options.parseBase64 && this._isBase64(element[key])) || options.parseBase64byIds?.includes(key) || options.parseBase64byIdsToHex?.includes(key)) { try { let value = Buffer.from(element[key], 'base64').toString('utf8'); if (options.parseBase64byIdsToHex?.includes(key)) { value = Buffer.from(element[key], 'base64').toString('hex'); } if (this._isJsonString(element[key])) { value = JSON.parse(element[key]); } element[key] = value; } catch (error) { this.adapter.log.warn(`Cannot parse base64 for ${path}.${key}: ${error}`); } } if (Array.isArray(element[key])) { await this._extractArray(element, key, path, options); } else if (element[key] !== null && typeof element[key] === 'object') { await this.parse(`${path}.${key}`, element[key], options); } else { const pathKey = key.replace(/\./g, '_'); if (!this.alreadyCreatedObjects[`${path}.${pathKey}`] || this.objectTypes[`${path}.${pathKey}`] !== typeof element[key]) { let objectName = key; if (options.descriptions?.[key]) { objectName = options.descriptions[key]; } let type = element[key] !== null ? typeof element[key] : 'mixed'; if (this.objectTypes[`${path}.${pathKey}`] && this.objectTypes[`${path}.${pathKey}`] !== typeof element[key]) { type = 'mixed'; this.adapter.log.debug(`Type changed for ${path}.${pathKey} from ${this.objectTypes[`${path}.${pathKey}`]} to ${type}`); } let states; if (options.states?.[key]) { states = options.states[key]; if (!states[element[key]]) { states[element[key]] = element[key]; } } const common = { name: objectName, role: this._getRole(element[key], options.write || false), type, write: options.write ?? true, read: true, states: states, }; if (options.units?.[key]) { common.unit = options.units[key]; // Assign the value to the 'unit' property } await this._createState(`${path}.${pathKey}`, common, options); } await this.adapter.setStateAsync(`${path}.${pathKey}`, element[key], true); } } } catch (error) { this.adapter.log.error(`Error extract keys: ${path} ${JSON.stringify(element)}`); this.adapter.log.error(error); } } /** * Creates a state object in the adapter's namespace. * * @param path - The path of the state object. * @param common - The common object for the state. * @param options - Optional parameters. * @param options.dontSaveCreatedObjects - If true, the created object will not be saved. * @returns - A promise that resolves when the state object is created. */ async _createState(path, common, options = {}) { path = path.toString().replace(this.forbiddenCharsRegex, '_'); await this.adapter .extendObjectAsync(path, { type: 'state', common, native: {}, }) .then(() => { if (!options.dontSaveCreatedObjects) { this.alreadyCreatedObjects[path] = true; } this.objectTypes[path] = common.type; }) .catch((error) => this.adapter.log.error(error)); } /** * Extracts an array from the given element and recursively parses its elements. * * @param element - The element containing the array. * @param key - The key of the array in the element. * @param path - The current path in the object hierarchy. * @param options - The parsing options. * @returns - A promise that resolves when the array extraction and parsing are complete. */ async _extractArray(element, key, path, options) { try { if (key) { element = element[key]; } for (let index in element) { let arrayElement = element[index]; if (arrayElement == null) { this.adapter.log.debug(`Cannot extract empty: ${path}.${key}.${index}`); continue; } let indexNumber = parseInt(index) + 1; index = indexNumber.toString(); if (indexNumber < 10) { index = `0${index}`; } if (options.autoCast && typeof arrayElement === 'string' && this._isJsonString(arrayElement)) { try { element[index] = JSON.parse(arrayElement); arrayElement = element[index]; } catch (error) { this.adapter.log.warn(`Cannot parse json value for ${path}.${key}.${index}: ${error}`); } } let arrayPath = key + index; if (typeof arrayElement === 'string' && key !== '') { // create a channel await this.adapter.extendObjectAsync(`${path}.${key}`, { type: 'channel', common: { name: key, }, native: {}, }, options); await this.parse(`${path}.${key}.${arrayElement.replace(/\./g, '')}`, arrayElement, options); continue; } const arrayElementKeys = Object.keys(arrayElement); if (typeof arrayElement[arrayElementKeys[0]] === 'string') { arrayPath = arrayElement[arrayElementKeys[0]]; } for (const keyName of arrayElementKeys) { if (keyName.endsWith('Id') && arrayElement[keyName] !== null) { if (arrayElement[keyName]?.replace) { arrayPath = arrayElement[keyName].replace(/\./g, ''); } else { arrayPath = arrayElement[keyName]; } } } for (const keyName of arrayElementKeys) { if (keyName.endsWith('Name')) { if (arrayElement[keyName]?.replace) { arrayPath = arrayElement[keyName].replace(/\./g, ''); } else { arrayPath = arrayElement[keyName]; } } } if (arrayElement.id) { if (arrayElement.id.replace) { arrayPath = arrayElement.id.replace(/\./g, ''); } else { arrayPath = arrayElement.id; } } if (arrayElement.name) { arrayPath = arrayElement.name.replace(/\./g, ''); } if (arrayElement.label) { arrayPath = arrayElement.label.replace(/\./g, ''); } if (arrayElement.labelText) { arrayPath = arrayElement.labelText.replace(/\./g, ''); } if (arrayElement.start_date_time) { arrayPath = arrayElement.start_date_time.replace(/\./g, ''); } if (options.preferredArrayName?.includes('+')) { const preferredArrayNameArray = options.preferredArrayName.split('+'); if (arrayElement[preferredArrayNameArray[0]] !== undefined) { const element0 = arrayElement[preferredArrayNameArray[0]] .toString() .replace(/\./g, '') .replace(/ /g, ''); let element1 = ''; if (preferredArrayNameArray[1].indexOf('/') !== -1) { const subArray = preferredArrayNameArray[1].split('/'); const subElement = arrayElement[subArray[0]]; if (subElement && subElement[subArray[1]] !== undefined) { element1 = subElement[subArray[1]]; } else if (arrayElement[subArray[1]] !== undefined) { element1 = arrayElement[subArray[1]]; } } else { element1 = arrayElement[preferredArrayNameArray[1]] .toString() .replace(/\./g, '') .replace(/ /g, ''); } arrayPath = `${element0}-${element1}`; } } else if (options.preferredArrayName?.includes('/')) { const preferredArrayNameArray = options.preferredArrayName.split('/'); const subElement = arrayElement[preferredArrayNameArray[0]]; if (subElement) { arrayPath = subElement[preferredArrayNameArray[1]] .toString() .replace(/\./g, '') .replace(/ /g, ''); } } else if (options.preferredArrayName && arrayElement[options.preferredArrayName]) { arrayPath = arrayElement[options.preferredArrayName].toString().replace(/\./g, ''); } if (options.forceIndex) { if (options.zeroBasedArrayIndex === true) { indexNumber -= 1; } if (options.disablePadIndex) { index = indexNumber.toString(); } else { // reassign index in case zeroBasedArrayIndex is enabled index = `${indexNumber < 10 ? '0' : ''}${indexNumber}`; } arrayPath = key + index; } // special case array with 2 string objects if (!options.forceIndex && arrayElementKeys.length === 2 && typeof arrayElementKeys[0] === 'string' && typeof arrayElementKeys[1] === 'string' && typeof arrayElement[arrayElementKeys[0]] !== 'object' && typeof arrayElement[arrayElementKeys[1]] !== 'object' && arrayElement[arrayElementKeys[0]] !== 'null') { // create a channel await this.adapter.extendObjectAsync(`${path}.${key}`, { type: 'channel', common: { name: key, }, native: {}, }, options); let subKey = arrayElement[arrayElementKeys[0]]; let subValue = arrayElement[arrayElementKeys[1]]; if ((options.parseBase64 && this._isBase64(subValue)) || options.parseBase64byIds?.includes(subKey) || options.parseBase64byIdsToHex?.includes(subKey)) { try { let value = Buffer.from(subValue, 'base64').toString('utf8'); if (options.parseBase64byIdsToHex?.includes(subKey)) { value = Buffer.from(subValue, 'base64').toString('hex'); } if (this._isJsonString(subValue)) { value = JSON.parse(subValue); } subValue = value; } catch (error) { this.adapter.log.warn(`Cannot parse base64 value ${subValue} for ${path}.${subKey}: ${error}`); } } const subName = `${Object.keys(arrayElement)[0]} ${Object.keys(arrayElement)[1]}`; if (key) { subKey = `${key}.${subKey || Object.keys(arrayElement)[0]}`; } if (!this.alreadyCreatedObjects[`${path}.${subKey}`] || this.objectTypes[`${path}.${subKey}`] !== typeof subValue) { let type = subValue !== null ? typeof subValue : 'mixed'; if (this.objectTypes[`${path}.${subKey}`] && this.objectTypes[`${path}.${subKey}`] !== typeof subValue) { this.adapter.log.debug(`Type of ${path}.${subKey} changed from ${this.objectTypes[`${path}.${subKey}`]} to ${typeof subValue}!`); type = 'mixed'; } let states; if (options.states?.[subKey]) { states = options.states[subKey]; states[subValue] ||= subValue; } let name = subName; if (options.descriptions?.[subKey.split('.').pop()]) { name = options.descriptions[subKey.split('.').pop()]; } const common = { name, role: this._getRole(subValue, options.write || false), type, write: options.write ?? true, read: true, states, }; if (options.units?.[subKey.split('.').pop()]) { common.unit = options.units[subKey.split('.').pop()]; } await this._createState(`${path}.${subKey}`, common, options); } await this.adapter.setStateAsync(`${path}.${subKey}`, subValue, true); continue; } await this.parse(`${path}.${arrayPath}`, arrayElement, options); } } catch (error) { this.adapter.log.error(`Cannot extract array ${path}`); this.adapter.log.error(error); } } /** * Checks if a string is a valid base64 encoded string. * * @param str - The string to be checked. * @returns - Returns true if the string is a valid base64 encoded string, otherwise returns false. */ _isBase64(str) { if (!str || typeof str !== 'string') { return false; } const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))/; return base64regex.test(str); } /** * Checks if a given string is a valid JSON string. * * @param str - The string to be checked. * @returns - Returns true if the string is a valid JSON string, otherwise false. */ _isJsonString(str) { try { JSON.parse(str); } catch { return false; } return true; } /** * Determines the role of an element based on its type and write mode. * * @param element - The element to determine the role for. * @param write - Indicates whether the element is being written to. * @returns - The role of the element. */ _getRole(element, write) { if (typeof element === 'boolean' && !write) { return 'indicator'; } if (typeof element === 'boolean' && write) { return 'switch'; } if (typeof element === 'number' && !write) { if (element && element.toString().length === 13) { if (element > 1500000000000 && element < 2000000000000) { return 'value.time'; } } else if (element && element.toFixed().toString().length === 10) { if (element > 1500000000 && element < 2000000000) { return 'value.time'; } } return 'value'; } if (typeof element === 'number' && write) { return 'level'; } if (typeof element === 'string') { return 'text'; } return 'state'; } } //# sourceMappingURL=json2iob.js.map