iobroker.lovelace
Version:
With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI
412 lines (391 loc) • 15.6 kB
JavaScript
/**
* Class to handle the entity registry.
* i.e. it needs to keep an array of entities, both manually configured and automatically detected.
* It needs to send the list upon request from the frontend.
* It has to store additional settings for each entity, like the history parser, the icon, the friendly name, etc.
*
* TODO: It should deprecate the dataSingleton.js file.
*/
const { utils } = require('../entities/utils');
/**
* EntityRegistry, module to implement entity registry and process messages from the frontend.
*
* Will store information about entities in ioBroker object database.
*/
class EntityRegistry {
_entries = {};
_entityCategories = { 0: 'config', 1: 'diagnostic' };
/**
* Constructor
*
* @param options {object} options including adapter.
*/
constructor(options) {
this.adapter = options.adapter;
this.entityData = options.entityData;
this.sendResponse = options.sendResponse;
this.sendUpdate = options.sendUpdate;
}
//LATER!!
/**
* Get entity by id
*
* @param id {string} the id of the entity
* @returns entity {object|undefined} the entity object or undefined if not found
*/
/*getEntity(id) {
return this._entities[id];
}*/
/**
* Set entity by id
*
* @param id {string} the id of the entity
* @param entity {object} the entity object
*/
/*setEntity(id, entity) {
this._entities[id] = entity;
}*/
/**
* Iterate over the entities
*
*/
/*[Symbol.iterator]() {
let index = 0;
const entities = this._entities;
return {
next() {
if (index < entities.length) {
return { value: entities[index++], done: false };
} else {
return { done: true };
}
},
};
}*/
/**
* Load the entity registry from the ioBroker object database
*
* @returns {Promise<void>}
*/
async loadEntityRegistry() {
const storage = await this.adapter.getObjectAsync('entityRegistry');
this._entries = storage?.native?.entities || {};
this._entityCategories = storage?.native?.entityCategories || { 0: 'config', 1: 'diagnostic' };
}
/**
* Store the entity registry from the ioBroker object database
*
* @returns {Promise<void>}
*/
async saveEntityRegistry() {
const storage = await this.adapter.getObjectAsync('entityRegistry');
if (!storage.native) {
storage.native = {};
}
storage.native.entities = this._entries;
storage.native.entityCategories = this._entityCategories;
await this.adapter.setObject('entityRegistry', storage);
}
/**
* Convert an entity registry entry to the format expected by the frontend for display.
*
* @param entityWithId {object} the entity object
* @returns {{ei: (*|string), en, ai: *, ic, di: *, lb: *[], hb: *, ec: *, tk: (string|*), pl, dp: *}} the entity in the format expected by the frontend
*/
convertEntryForDisplay(entityWithId) {
return {
ei: entityWithId.entity_id,
en: entityWithId.name || entityWithId.original_name,
ai: entityWithId.area_id,
ic: entityWithId.icon || entityWithId.original_icon,
di: entityWithId.device_id,
lb: entityWithId.labels || [],
hb: entityWithId.hidden,
ec: entityWithId.entity_category,
tk: entityWithId.translation_key,
pl: entityWithId.platform,
dp: entityWithId.display_precision,
};
}
/**
* Create an entity with an id. Fills all those possible settings in the store with default values or values taken
* from an entity.
*
* @param entity {object} the entity object to create the entry for.
* @returns {Promise<void>}
*/
_createEntryFromEntity(entity) {
const entry = {
id: entity.context.id,
entity_id: entity.entity_id,
name: null, //seems to always be null?
icon: null,
platform: entity.platform,
config_entry_id: null,
config_subentry_id: null,
device_id: entity.context.deviceId || null,
area_id: entity.context.roomId || null,
labels: [],
disabled_by: null, //"user" | "device" | "integration" | "config_entry"
hidden_by: null, //"user" | "device" | "integration"
entity_category: null, // "config" | "diagnostic" ...?
has_entity_name: false, //what...??? does not change to true after changing name...??
original_name: entity.attributes?.friendly_name, //seems to always be the same as name
unique_id: entity.context.id, //quite different entries.. seem to contain id part of entity_id sometimes. Hm.
translation_key: null, //can we control translation with that... but... what translation?
options: null, //seem to be "platform" specific options. Hm.
categories: {},
capabilities: entity.context.capabilities || null,
original_icon: entity.attributes?.icon, //seems to always be the same as icon
device_class: entity.attributes?.device_class || null,
original_device_class: entity.attributes?.device_class || null,
aliases: entity.context.aliases || null,
};
if (entry.platform === 'sensor') {
//sensor options -> see entity_registry.ts
entry.options = {
sensor: {
display_precision: entity.attributes?.display_precision || null,
suggested_display_precision: entity.attributes?.suggested_display_precision || null,
unit_of_measurement: entity.attributes?.unit_of_measurement || null, //also number option
},
};
} else if (entry.platform === 'number') {
entry.options = {
number: {
unit_of_measurement: entity.attributes?.unit_of_measurement || null, //also number option
},
};
} else if (entry.platform === 'light') {
//light options -> see entity_registry.ts
entry.options = {
light: {
favorite_colors: entity.attributes?.favorite_colors || [],
},
};
} else if (entry.platform === 'lock') {
//lock options:
entry.options = {
lock: {
default_code: entity.attributes?.default_code || null,
},
};
} else if (entry.platform === 'alarm_control_panel') {
entry.options = {
alarm_control_panel:
entry.platform === 'alarm_control_panel'
? {
default_code: entity.attributes?.default_code || null,
}
: undefined,
};
} else if (entry.platform === 'weather') {
entry.options = {
weather: {
//weather options:
precipitation_unit: entity.attributes?.precipitation_unit || undefined,
pressure_unit: entity.attributes?.pressure_unit || undefined,
temperature_unit: entity.attributes?.temperature_unit || undefined,
visibility_unit: entity.attributes?.visibility_unit || undefined,
wind_speed_unit: entity.attributes?.wind_speed_unit || undefined,
},
};
}
return entry;
}
/**
* Update the entity with the new values from the registry. Happens if the frontend changes something.
* But it should also happen if the entity is (re)created.
*
* @param entity {object} the entity to update
* @param [entry] {object|undefined} the registry entry to take the data from.
*/
updateEntityFromRegistry(entity, entry) {
if (!entry) {
entry = this._entries[entity.context.id];
if (!entry) {
this.adapter.log.warn(`Entity ${entity.context.id} not found in registry`);
return;
}
}
entity.entity_id = entry.entity_id;
this.entityData.entityId2Entity[entry.entity_id] = entity;
entity.attributes.friendly_name = entry.name || entry.original_name;
entity.attributes.icon = entry.icon || entry.original_icon;
entity.platform = entry.platform;
entity.attributes.device_class = entry.device_class || entry.original_device_class;
if (entry.options) {
for (const platform of Object.keys(entry.options)) {
if (entry.options[platform]) {
for (const attribute of Object.keys(entry.options[platform])) {
entity.attributes[attribute] = entry.options[platform][attribute];
}
}
}
}
}
/**
* Get the entity id from the iobroker id.
*
* @param iobId {string} the ioBroker id
*/
getEntityId(iobId) {
return this._entries[iobId]?.entity_id;
}
/**
* Store the entity id in the registry for this iob id to solve duplicates.
*
* @param iobId {string} the ioBroker id
* @param entityId {string} the entity id
*/
storeEntityId(iobId, entityId) {
this._entries[iobId] = this._entries[iobId] || {};
this._entries[iobId].entity_id = entityId;
}
/**
* Process incoming messages from the frontend
*
* @param ws {WebSocket} the websocket connection
* @param message {object} the message from the frontend
* @returns {Promise<boolean>} true if the message was processed, false if not
*/
async processMessage(ws, message) {
if (message.type === 'config/entity_registry/list_for_display') {
const entities = [];
for (const entity of this.entityData.entities) {
entities.push(this.convertEntryForDisplay(this._createEntryFromEntity(entity)));
}
this.sendResponse(ws, message.id, {
entities,
entity_categories: this._entityCategories,
});
return true;
} else if (message.type === 'config/entity_registry/list') {
const entities = [];
for (const id of Object.keys(this._entries)) {
entities.push(this._entries[id]);
}
this.sendResponse(ws, message.id, {
entities,
entity_categories: this._entityCategories,
});
return true;
} else if (message.type === 'config/entity_registry/get') {
const entityId = message.entity_id;
let entityWithId = this._entries[entityId];
const entity = this.entityData.entityId2Entity[entityId];
if (!entity) {
ws.send(
JSON.stringify({
id: message.id,
type: 'result',
success: false,
error: { code: 'entity_not_found' },
}),
);
this.adapter.log.warn(`Entity ${entityId} not found`);
return true;
}
if (!entityWithId) {
entityWithId = this._createEntryFromEntity(entity);
}
this.sendResponse(ws, message.id, entityWithId);
return true;
} else if (message.type === 'config/entity_registry/get_entries') {
const entityIds = message.entity_ids;
const result = {};
for (const entityId of entityIds) {
const entityWithId = this._entries[entityId];
const entity = this.entityData.entityId2Entity[entityId];
if (entityWithId) {
result[entityId] = entityWithId;
} else if (entity) {
result[entityId] = this._createEntryFromEntity(entity);
} else {
result[entityId] = null;
}
}
this.sendResponse(ws, message.id, result);
return true;
} else if (message.type === 'config/entity_registry/update') {
const entityId = message.entity_id;
const entityWithId = this._entries[entityId];
const entity = this.entityData.entityId2Entity[entityId];
if (!entity) {
ws.send(
JSON.stringify({
id: message.id,
type: 'result',
success: false,
error: { code: 'entity_not_found' },
}),
);
this.adapter.log.warn(`Entity ${entityId} not found`);
return true;
}
if (!entityWithId) {
this._createEntryFromEntity(entity);
}
// deep copy message:
const newData = JSON.parse(JSON.stringify(message));
delete newData.id;
delete newData.type;
delete newData.entity_id;
const changes = {};
for (const key of Object.keys(newData)) {
changes[key] = entityWithId[key] || null; //notify about changes.
entityWithId[key] = newData[key];
if (key === 'new_entity_id') {
//to replace entity_id, need to touch some caches.
utils.removeEntity(entity, newData[key]);
delete entityWithId.new_entity_id;
}
}
this.updateEntityFromRegistry(entity, entityWithId);
this.sendResponse(ws, message.id, {
entity_entry: entityWithId,
});
this.sendUpdate('entity_registry_updated', {
action: 'update',
entity_id: entityWithId.entity_id,
changes,
});
}
//for now handle device registry here, too.
//need to also send `device_registry_updated` event.
return false;
}
/**
* Update entities from the registry. Inform the frontend about changes.
*
* @param entities {[]} the entities to update
* @param sendEvent {boolean} true if the event should be sent
*/
handleUpdatedEntities(entities = [], sendEvent = false) {
if (entities.length > 0 && sendEvent) {
for (const entity of entities) {
this.updateEntityFromRegistry(entity);
}
this.sendUpdate('entity_registry_updated');
this.sendUpdate('device_registry_updated'); //for now, do that here. Other module is not notified about entity changes.
}
}
/**
* Clean up, save the entity registry
*
* @returns {Promise<void>}
*/
async cleanup() {
this.adapter.log.debug('cleaning up entity registry');
await this.saveEntityRegistry();
}
/**
* Init module.
*
* @returns {Promise<void>}
*/
async init() {
await this.loadEntityRegistry();
}
}
module.exports = EntityRegistry;