UNPKG

iobroker.lovelace

Version:

With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI

531 lines (490 loc) 17.8 kB
const pinyin = require('pinyin'); const entityData = require('./../dataSingleton'); /** * Creates objects-Object used in entity creation from array of ids. * @param {Array<string>} ids array of ids * @returns {Promise<{string: ioBroker.Object}>} */ async function createObjectsFromArrayOfIds(ids) { const objects = {}; const objectsUnsorted = await Promise.all(ids.map(id => entityData.adapter.getForeignObjectAsync(id))); for (const obj of objectsUnsorted) { if (obj) { //might be null if object for id does not exist. objects[obj._id] = obj; } } return objects; } /** * Finds out if id is part of this enum or not. * @param e * @param id * @returns {boolean} */ function isIdInEnum(e, id) { for (const member of e.common.members) { if (id.startsWith(member)) { return true; } } return false; } /** * Find enum in enums array for id. * @param enums * @param id * @returns {Object|undefined} */ function findEnumForId(enums, id) { for (const e of enums) { if (isIdInEnum(e, id)) { return e; } } } /** * Gets smartName for object or for id from objects cache. * @param {{String: ioBroker.Object}|ioBroker.Object} objects id to object cache * @param {String} id id of object to get the smartName for. * @returns {string|undefined} */ function getSmartName(objects, id) { if (!id) { return objects.common.smartName; } return objects[id] && objects[id].common ? objects[id].common.smartName : undefined; } /** * Get name of enum * @param {ioBroker.Enum} obj * @param {string} [lang] or 'en' * @returns {string} */ function getEnumName(obj, lang) { let name = getSmartName(obj); name = name || obj.common.name; if (name && typeof name === 'object') { name = name[lang] || name.en; } if (!name) { const parts = obj._id.split('.'); parts.unshift(); //remove "enum" parts.unshift(); //remove "functions" / "rooms" name = ''; for (const part of parts) { name += ' ' + part[0].toUpperCase() + part.substring(1); } name = name.substring(1); //remove first space. } return name; } /** * Internal function of setJsonAttribute * @param obj * @param parts * @param index * @param value * @private */ function _setJsonAttribute(obj, parts, index, value) { if (parts.length - 1 === index) { obj[parts[index]] = value; } else { // if a number if (typeof obj[parts[index]] !== 'object') { if (parts.length - 2 >= index && parts[index + 1] >= '0' && parts[index + 1] <= '9') { obj[parts[index]] = []; } else { obj[parts[index]] = {}; } } _setJsonAttribute(obj[parts[index]], parts, index + 1, value); } } /** * sets attribute of entity. * @param obj * @param path * @param value */ function setJsonAttribute(obj, path, value) { if (!path) { entityData.log.error('Invalid attribute name for ' + JSON.stringify(obj) + ' = ' + value); return; } const parts = path.split('.'); if (parts.length === 1) { obj[path] = value; } else { _setJsonAttribute(obj, parts, 0, value); } } /** * Replaces invalid characters from generated unique part of entity id, i.e. light.idPart * @param idPart unique part of entity Id * @returns {string} cleaned entity Id part. */ function _replaceInvalidChars(idPart) { idPart = idPart.replace(/Ü/g, 'UE'); idPart = idPart.replace(/Ä/g, 'AE'); idPart = idPart.replace(/Ö/g, 'OE'); idPart = idPart.replace(/ü/g, 'ue'); idPart = idPart.replace(/ä/g, 'ae'); idPart = idPart.replace(/ö/g, 'oe'); idPart = idPart.replace(/ß/g, 'ss'); idPart = idPart.replace(/[^a-zA-Z0-9А-Яа-я_]/g, '_'); return idPart; } /** * Get Icon if object has one. Supports data:image, url, and local icon path. * @param obj * @param prefix * @returns {string|null|*} * @private */ function _getObjectIcon(obj, prefix) { prefix = prefix || '.';//http://localhost:8081'; if (!obj || !obj.common || !obj.common.icon) { return null; } let icon; if (!obj.common.icon.match(/^data:image\//)) { if (obj.common.icon.startsWith('http')) { //icon is full URL. Use that. return obj.common.icon; } if (obj.common.icon.indexOf('.') !== -1) { let instance; if (obj.type === 'instance') { icon = prefix + '/adapter/' + obj.common.name + '/' + obj.common.icon; } else if (obj._id && obj._id.match(/^system\.adapter\./)) { instance = obj._id.split('.', 3); if (obj.common.icon[0] === '/') { instance[2] += obj.common.icon; } else { instance[2] += '/' + obj.common.icon; } icon = prefix + '/adapter/' + instance[2]; } else { instance = obj._id.split('.', 2); if (obj.common.icon[0] === '/') { instance[0] += obj.common.icon; } else { instance[0] += '/' + obj.common.icon; } icon = prefix + '/adapter/' + instance[0]; } } else { return null; // '<i class="material-icons iob-list-icon">' + obj.common.icon + '</i>'; } } else { // base 64 image icon = obj.common.icon; } return icon; } /** * ioBroker ID needs to be subscribed, if entity is used in vis in order to update. * @param id of ioBroker object * @param entity complete entity */ function addID2entity(id, entity) { if (!entity.context.ids) { entity.context.ids = []; } if (!entity.context.ids.includes(id)) { entity.context.ids.push(id); } } /** * Get name from object, depending on lang with fall back to id. * @param obj * @param _lang * @returns {string|*} * @private */ function _getObjectName(obj, _lang) { _lang = _lang || entityData.lang; if (obj && obj.common && obj.common.name) { if (typeof obj.common.name === 'object') { if (obj.common.name[_lang] || obj.common.name.en) { return obj.common.name[_lang] || obj.common.name.en; } else { const lang = Object.keys(obj.common.name).find(lang => obj.common.name[lang]); if (obj.common.name[lang]) { return obj.common.name[lang]; } else { return obj._id; } } } else { return obj.common.name; } } else { return obj ? obj._id || '' : ''; } } /** * Generates name from object depending on language. * @param obj * @param __lang * @returns {string|*|string} */ function getObjectName(obj, __lang) { const objName = _getObjectName(obj, __lang); const pinyinObjNameObj = pinyin(objName, {style: pinyin.STYLE_TONE2}); if (typeof pinyinObjNameObj === 'object' && pinyinObjNameObj.length > 1) { // Found Chinese word. // "Chinese中文" => [ [ 'Chinese' ], [ 'zhong1' ], [ 'wen2' ] ] let pinyinObjName = ''; for (let i = 0; i < pinyinObjNameObj.length; i++) { if (typeof pinyinObjNameObj[i] === 'object') { pinyinObjName += pinyinObjNameObj[i][0]; } } return pinyinObjName; } // No Chinese word found, return origin object name return objName; } /** * Generates name from obj and replaces some characters so it is valid friendly name. * @param obj * @param lang * @returns {string} * @private */ function _generateName(obj, lang) { return getObjectName(obj, lang).replace(/[^-._\w0-9А-Яа-яÄÜÖßäöü ]/g, '_'); } /** * Removes entity from cached storages. * @param entity */ function removeEntity(entity) { // need to process those: // entityData.entities = []; // entityData.entityId2Entity = {}; // entityData.iobID2entity = {}; // entityData._entityIconUrls = []; //stores icon urls that may be accessed without token. if (!entity) { return; } delete entityData.entityId2Entity[entity.entity_id]; let foundIndex = entityData.entities.findIndex(x => x.entity_id === entity.entity_id); while (foundIndex !== -1) { entityData.entities.splice(foundIndex, 1); foundIndex = entityData.entities.findIndex(x => x.entity_id === entity.entity_id); } if (entity.attributes.entity_picture) { const urlIndex = entityData.entityIconUrls.findIndex(x => x === entity.attributes.entity_picture); if (urlIndex !== -1) { entityData.entityIconUrls.splice(urlIndex, 1); } } for (const key of Object.keys(entityData.iobID2entity)) { const entities = entityData.iobID2entity[key]; let foundIndex = entities.findIndex(x => x.entity_id === entity.entity_id); while (foundIndex !== -1) { entities.splice(foundIndex, 1); foundIndex = entities.findIndex(x => x.entity_id === entity.entity_id); } } } /** * Find all entities that are affected by a change in an enum (or that are members of an enum, if oldEnum is empty) * @param {ioBroker.Enum} newEnum * @param {ioBroker.Enum} [oldEnum] * @returns {Array<Entity>} */ function findEntitiesFromEnumChange(newEnum, oldEnum) { const membersNew = newEnum && newEnum.common ? newEnum.common.members || [] : []; const membersOld = oldEnum && oldEnum.common ? oldEnum.common.members || [] : []; let entities = []; const ids = []; for (const id of membersNew) { if (!membersOld.includes(id)) { //new id! const affectedEntities = entityData.iobID2entity[id]; if (affectedEntities && affectedEntities.length) { entities = entities.concat(affectedEntities); } else { ids.push(id); } } } for (const id of membersOld) { if (!membersNew.includes(id)) { //removed id! const affectedEntities = entityData.iobID2entity[id]; if (affectedEntities && affectedEntities.length) { entities = entities.concat(affectedEntities); } else { ids.push(id); } } } return {ids, entities}; } /** * generate a bare entity from parameters. Already adds the entity to a bunch of arrays. So be sure to use it! * @param {string|null} name friendly name of entity, if empty, will be read from object or generated from room & func or id. * @param {string|null} room room of device for name generation * @param {string|null} func function of device for name generation * @param {Object} obj ioBroker object, used to read id, name, icon, unit, lovelace specific settings. * @param {string} entityType lovelace domain of entity, for example light, sensor, ... * @param [entity_id] predefined entity id. If empty, will be generated from name * @returns {{context: {id: string, type: string}, attributes: {friendly_name: string}, entity_id: string}} */ function processCommon(name, room, func, obj, entityType, entity_id) { if (!name) { if (obj && obj.common && obj.common.name) { name = _generateName(obj, entityData.lang); } else if (func && room) { name = room + ' ' + func; } else { name = (obj && obj.common && obj.common.custom && obj.common.custom[entityData.adapter.namespace] && obj.common.custom[entityData.adapter.namespace].name) || _generateName(obj, entityData.lang); } } let idPart; if (entity_id && /^.+\..+$/.test(entity_id)) { /** @type{Array<String>} */ const parts = entity_id.split('.'); entityType = parts.shift(); idPart = _replaceInvalidChars(parts.join('_')); } else { idPart = _replaceInvalidChars(_generateName(obj, 'en')); } const entity = { entity_id: (entityType + '.' + idPart), attributes: { friendly_name: name }, //last_changed: new Date(state.lc).toISOString(), //last_updated: new Date(state.ts).toISOString(), context: { id: obj._id, type: entityType, room: room, func: func, ids: [obj._id] } }; if (obj.common && obj.common.unit) { entity.attributes.unit_of_measurement = obj.common.unit; } if (obj.common && obj.common.icon) { entity.attributes.entity_picture = _getObjectIcon(obj); } addID2entity(obj._id, entity); return entity; } /** * Fill entity functions from states Object -> allows users to add generic iobroker ids as attributes. Useful for additional info (currently, only). * @param {Record<string, string>} states ids of iobroker states - "state" goes into entity-state, can also have stateRead. Rest creates attributes from key. * @param {Entity} entity entity to fill. */ function fillEntityFromStates(states, entity) { //state: entity.context.STATE = {setId: states.state || null, getId: states.stateRead || states.state || null}; //attributes: entity.context.ATTRIBUTES = entity.context.ATTRIBUTES || []; for (const key of Object.keys(states)) { const id = states[key]; if (id) { addID2entity(id, entity); if (key !== 'state' && key !== 'stateRead') { entity.context.ATTRIBUTES.push({attribute: key, getId: id}); } } } } /** * Determine entity type if none set. * @param {ioBroker.Object} obj * @returns {string} */ function autoDetermineEntityType(obj) { if (obj.common) { if (obj.common.write) { if (obj.common.states) { return 'input_select'; } if (obj.common.role === 'date') { return 'input_datetime'; } if (obj.common.type === 'number') { return 'input_number'; } else if (obj.common.type === 'boolean') { return 'input_boolean'; } else { return 'input_text'; } } else { if (obj.common.type === 'boolean') { return 'binary_sensor'; } else { return 'sensor'; } } } else { return 'sensor'; //sensor can be everything. } } /** * Fill entity into entityData-entity caches. * @param entity */ function fillEntityIntoCaches(entity) { const foundIndex = entityData.entities.findIndex(x => x.entity_id === entity.entity_id); if (foundIndex !== -1) { entityData.log.warn(`Got duplicate for entity ${entity.entity_id}. Overwriting old value. Was for ${entityData.entities[foundIndex].context.id} and new one is for ${entity.context.id}`); entityData.entities[foundIndex] = entity; } else { entityData.entities.push(entity); } entityData.entityId2Entity[entity.entity_id] = entity; for (const id of entity.context.ids) { entityData.iobID2entity[id] = entityData.iobID2entity[id] || []; const foundIndex = entityData.iobID2entity[id].findIndex(e => e.entity_id === entity.entity_id); if (foundIndex === -1) { entityData.iobID2entity[id].push(entity); } else { entityData.iobID2entity[id][foundIndex] = entity; } } } /** * Creates name of entity from "custom" part of ioBroker object. * @param {ioBroker.Object} obj * @param {String} namespace - namespace of adapter * @returns {string} */ function createEntityNameFromCustom(obj, namespace) { if (!obj.common || !obj.common.custom || !obj.common.custom[namespace]) { return autoDetermineEntityType(obj) + '.' + _replaceInvalidChars(_generateName(obj, 'en')); } const custom = obj.common.custom[namespace]; const entityType = custom.entity || autoDetermineEntityType(obj); const idPart = custom.name; const entity_id = idPart && typeof idPart === 'string' ? entityType + '.' + idPart : null; return entity_id; } //required to read user name exports.getObjectName = getObjectName; exports.getEnumName = getEnumName; //required by other converters: exports.processCommon = processCommon; exports.addID2entity = addID2entity; exports.fillEntityFromStates = fillEntityFromStates; //other tools: exports.createObjectsFromArrayOfIds = createObjectsFromArrayOfIds; exports.createEntityNameFromCuston = createEntityNameFromCustom; exports.autoDetermineEntityType = autoDetermineEntityType; exports.setJsonAttribute = setJsonAttribute; exports.getSmartName = getSmartName; exports.fillEntityIntoCaches = fillEntityIntoCaches; //used for intelligent update: exports.removeEntity = removeEntity; exports.findEnumForId = findEnumForId; exports.findEntitiesFromEnumChange = findEntitiesFromEnumChange;