UNPKG

iobroker.lovelace

Version:

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

1,227 lines 117 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var import_node_fs = __toESM(require("node:fs")); var import_node_crypto = __toESM(require("node:crypto")); var import_node_path = __toESM(require("node:path")); var import_adapter_core = require("@iobroker/adapter-core"); var import_autoEntities = require("./modules/autoEntities"); var utils = __toESM(require("./entities/utils")); var import_baseEntity = require("./entities/baseEntity"); var import_friendly_name = require("./entities/friendly_name"); var import_sun = require("./sun"); var import_genericConverter = require("./converters/genericConverter"); var import_converter = require("./converters/converter"); var converterSwitch = __toESM(require("./converters/switch")); var converterBinarySensors = __toESM(require("./converters/binary_sensor")); var converterSensors = __toESM(require("./converters/sensor")); var converterGeoLocation = __toESM(require("./converters/geo_location")); var converterDeviceTracker = __toESM(require("./converters/deviceTracker")); var import_syntheticControl = require("./converters/syntheticControl"); var converterDatetime = __toESM(require("./converters/input_datetime")); var converterAlarmCP = __toESM(require("./converters/alarm_control_panel")); var converterInputSelect = __toESM(require("./converters/input_select")); var convertFan = __toESM(require("./converters/fan")); var converterClimate = __toESM(require("./converters/climate")); var converterLight = __toESM(require("./converters/light")); var import_lock = require("./converters/lock"); var import_camera = require("./converters/camera"); var import_weather = require("./converters/weather"); var import_cover = require("./converters/cover"); var import_vacuum = require("./converters/vacuum"); var import_humidifier = require("./converters/humidifier"); var import_water_heater = require("./converters/water_heater"); var import_media_player = require("./converters/media_player"); var import_browser_mod = __toESM(require("./modules/browser_mod")); var import_history = __toESM(require("./modules/history")); var import_conversation = __toESM(require("./modules/conversation")); var import_logbook = __toESM(require("./modules/logbook")); var import_persistentNotifications = __toESM(require("./modules/persistentNotifications")); var import_todo = __toESM(require("./modules/todo")); var import_person = __toESM(require("./modules/person")); var import_statisticsRecorder = __toESM(require("./modules/statisticsRecorder")); var import_entityRegistry = __toESM(require("./modules/entityRegistry")); var import_dashboard = __toESM(require("./modules/dashboard")); var import_deviceRegistry = __toESM(require("./modules/deviceRegistry")); var import_areaRegistry = __toESM(require("./modules/areaRegistry")); var import_energyModule = __toESM(require("./modules/energyModule")); var import_userData = __toESM(require("./modules/userData")); var import_themes = __toESM(require("./modules/themes")); var import_panels = __toESM(require("./panels")); var import_template = __toESM(require("./modules/template")); var import_compat = __toESM(require("./modules/compat")); var import_mediaSource = __toESM(require("./modules/mediaSource")); var import_search = __toESM(require("./modules/search")); var import_image = __toESM(require("./modules/image")); var import_calendar = __toESM(require("./modules/calendar")); var import_storage = require("./modules/storage"); const WebSocket = require("ws"); const bodyParser = require("body-parser"); const multer = require("multer"); const mime = require("mime"); const jstz = require("jstimezonedetect"); const entityData = require("../../lib/dataSingleton"); const ChannelDetector = require("@iobroker/type-detector").default; const CONVERTIBLE_UNITS = { energy: ["Wh", "kWh", "MWh", "GWh", "TWh", "J", "kJ", "MJ", "GJ", "cal", "kcal", "Mcal", "Gcal"], power: ["mW", "W", "kW", "MW", "GW", "TW"], gas: ["L", "m\xB3", "ft\xB3", "CCF"], water: ["L", "mL", "m\xB3", "ft\xB3", "CCF", "gal", "fl. oz."], volume: ["L", "mL", "m\xB3", "ft\xB3", "CCF", "gal", "fl. oz."], temperature: ["\xB0C", "\xB0F", "K"], pressure: ["Pa", "hPa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], speed: ["m/s", "km/h", "mph", "ft/s", "kn"], distance: ["km", "m", "cm", "mm", "mi", "yd", "in", "ft", "nmi"] }; const TIMEOUT_PASSWORD_ENTER = 18e4; const TIMEOUT_AUTH_CODE = 1e4; const ROOT_DIR = "../../hass_frontend"; const VERSION = import_node_fs.default.readFileSync(`${getRootPath()}version.txt`, "utf8").replace(/(\d{4})(\d{2})(\d{2})\.(\d).*/s, "$1.$2.$3"); const NO_TOKEN = "no_token"; function getRootPath() { if (ROOT_DIR.match(/^\w:/) || ROOT_DIR.startsWith("/")) { return `${ROOT_DIR}/`; } return import_node_path.default.resolve(`${__dirname}/${ROOT_DIR}`) + import_node_path.default.sep; } const generateRandomToken = function(callback) { import_node_crypto.default.randomBytes(256, (_ex, buffer) => { import_node_crypto.default.randomBytes(32, (ex, secret) => { if (ex) { return callback("server_error"); } const token = import_node_crypto.default.createHmac("sha256", secret).update(buffer).digest("hex"); callback(false, token); }); }); }; const staticOptions = { maxAge: 2678400 * 1e3 // 31 days }; class WebServer { adapter; config; log; lang; detector; words; systemConfig; _lovelaceConfig; _ressourceConfig; _requestableFiles; _subscribed; /** true when we subscribed to all foreign states ('*') and filter in onStateChange instead. */ _subscribedAll = false; _server; _app; _auth_flows; _objectData; _modules; _wss; _indexHtml; _clearInterval; _sunInterval; _sunLocationWarned; _updateTimer; /** * Constructor of the WebServer class. * * @param options object with options from the adapter. */ constructor(options) { this._lovelaceConfig = null; this._ressourceConfig = []; this.adapter = options.adapter; this.config = this.adapter.config; this.log = this.adapter.log; this.lang = this.config.language || "en"; this.detector = new ChannelDetector(); this.config.ttl = parseInt(this.config.ttl, 10) || 3600; this.words = options.words || {}; entityData.adapter = this.adapter; entityData.log = this.adapter.log; entityData.words = this.words; entityData.server = this; entityData.autoEntityIdFormat = this.config.autoEntityIdFormat || "name"; this._requestableFiles = []; this._subscribed = []; this._server = options.server; this._app = options.app; this._auth_flows = {}; this._objectData = { objects: {}, //id -> object storage ids: [], //array of object ids. rooms: {}, functions: {}, roomNames: {}, //id -> name storage funcNames: {}, updatedIds: [], //temporary storage for updated ids usedKeys: [] //temporary storage for used keys (type-detector) }; const person = new import_person.default({ adapter: this.adapter }); this._modules = { browserMod: new import_browser_mod.default({ adapter: this.adapter, objects: this._objectData.objects }), conversation: new import_conversation.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), lang: this.lang, words: this.words }), logbook: new import_logbook.default({ adapter: this.adapter, getUsedEntityIDs: () => { const entities = []; this._flatJSON(this._lovelaceConfig ? this._lovelaceConfig.views : {}, entities); return entities; } }), notifications: new import_persistentNotifications.default({ adapter: this.adapter, server: this }), todo: new import_todo.default({ adapter: this.adapter, entityData, server: this, getWebsocketServer: () => this._wss }), person, entityRegistry: new import_entityRegistry.default({ adapter: this.adapter, entityData, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), sendUpdate: (type, data) => this._sendUpdate(type, data), renameEntityIdInConfigs: (oldId, newId) => this._renameEntityIdInConfigs(oldId, newId) }), dashboard: new import_dashboard.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), sendUpdate: (type) => this._sendUpdate(type) }), deviceRegistry: new import_deviceRegistry.default({ adapter: this.adapter, entityData, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result) }), areaRegistry: new import_areaRegistry.default({ adapter: this.adapter, rooms: this._objectData.rooms, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), sendUpdate: (type) => this._sendUpdate(type) }), energy: new import_energyModule.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result) }), userData: new import_userData.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result) }), themes: new import_themes.default({ adapter: this.adapter, sendUpdate: (type) => this._sendUpdate(type) }), template: new import_template.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), subscribeState: (id) => { if (this._subscribedAll) { return; } if (this._subscribed.indexOf(id) === -1) { this._subscribed.push(id); Promise.resolve(this.adapter.subscribeForeignStatesAsync(id)).catch( (e) => this.log.warn(`Could not subscribe to ${id}: ${String(e)}`) ); this.log.debug(`IoB Subscribe on ${id}`); } } }), compat: new import_compat.default({ sendResponse: (ws, id, result) => this._sendResponse(ws, id, result) }), mediaSource: new import_mediaSource.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result) }), search: new import_search.default({ sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), entityData }), calendar: new import_calendar.default({ adapter: this.adapter, sendResponse: (ws, id, result) => this._sendResponse(ws, id, result), entityData, getUserIDFromName: (name) => this._modules.person.getUserIDFromName(name) }), image: new import_image.default({ adapter: this.adapter, entityData, getUserIDFromName: (name) => this._modules.person.getUserIDFromName(name), resolveUser: ({ entityId, token, accessToken, url, reqUser, entity }) => { let userName; if (this.config.auth !== false && (token || accessToken) && !this._requestableFiles.includes(url) && !entityData.entityIconUrls.includes(url)) { if (accessToken) { const now = Date.now(); const flowId = Object.keys(this._auth_flows).find( (fId) => this._auth_flows[fId].access_token === accessToken && now - this._auth_flows[fId].ts < this._auth_flows[fId].auth_ttl ); if (!flowId) { throw new Error("Invalid token!"); } userName = this._auth_flows[flowId].username; } else if (token && (entity == null ? void 0 : entity.attributes.access_token) !== token) { this.log.warn(`Invalid access token for ${entityId} - ${token}`); throw new Error(`Invalid access token for ${entityId} - ${token}`); } else { userName = reqUser; } } return userName; } }), history: new import_history.default({ adapter: this.adapter, entityData, personModule: person }), statisticsRecorder: new import_statisticsRecorder.default({ adapter: this.adapter, server: this, log: this.log, personModule: person, dataSingleton: entityData }) }; if (this.adapter.config.updateTimeout !== void 0) { this.adapter.config.updateTimeout = Math.max(100, Math.min(this.adapter.config.updateTimeout, 3e4)); } const storageReady = (0, import_storage.migrateStorageObjects)(this.adapter); const entityRegistryReady = storageReady.then(() => this._modules.entityRegistry.init()); const concurrentPromises = [ this._modules.todo.init(), this._modules.person.init(), entityRegistryReady, storageReady.then(() => this._modules.areaRegistry.init()), storageReady.then(() => this._modules.energy.init()), storageReady.then(() => this._modules.dashboard.init()), storageReady.then(() => this._modules.userData.init()), this.adapter.getForeignObjectAsync("system.config").then((config) => { this.lang = this.config.language || config.common.language; entityData.lang = this.lang; this.systemConfig = config.common; this.systemConfig.ts = config.ts; this._updateConstantEntities(); return this.adapter.getObjectAsync("configuration"); }).then((config) => { if (config && config.native && config.native.title) { this._lovelaceConfig = config.native; } else { this._lovelaceConfig = require("../../lib/defaultConfig"); } }).then(() => this._modules.browserMod.init(this._lovelaceConfig)), entityRegistryReady.then(() => this._readAllEntities()), this._listFiles(), this._modules.themes.init() ]; Promise.all(concurrentPromises).then(() => { var _a; this.adapter.subscribeObjects("configuration"); this.adapter.subscribeStates("control.*"); this.adapter.subscribeStates("notifications.*"); this.adapter.subscribeStates("instances.*"); this.adapter.subscribeStates("conversation"); this._init(); for (const mod of Object.values(this._modules)) { (_a = mod.augmentServices) == null ? void 0 : _a.call(mod, entityData.services); } if (this.config.auth !== false) { this._clearInterval = setInterval(() => this.clearAuth(), 6e4); } this._sunInterval = setInterval(() => this._updateSunEntity(), 6e4); this.adapter.setState("info.readyForClients", true, true); this.log.debug("Initialization done."); }).catch((err) => { this.log.error(`Initialization error: ${err}`); if (typeof this.adapter.terminate === "function") { this.adapter.terminate(import_adapter_core.EXIT_CODES.INVALID_ADAPTER_CONFIG); } else { process.exit(import_adapter_core.EXIT_CODES.INVALID_ADAPTER_CONFIG); } }); } /** * Generate all entities from object database using type-detector and custom settings. * * @returns resolves, when done. */ async _readAllEntities() { const smartDevices = await this._updateDevices(); for (const entity of smartDevices) { entity.registerInCaches(); } await this._getManualEntities(); for (const entity of entityData.entities) { if (entity.attributes.entity_picture && !entity.attributes.entity_picture.match(/^data:image\//)) { const url = entity.attributes.entity_picture.replace(/^\./, ""); if (!entityData.entityIconUrls.includes(url)) { entityData.entityIconUrls.push(url); } } } await this._getAllStates(); await this._manageSubscribesFromConfig(); this.log.debug("entitiesUpdated for startup."); this.log.debug("entities: init done"); await this.adapter.setStateAsync("info.entitiesUpdated", true, true); } /** * Remove auth sessions that expired. */ clearAuth() { const now = Date.now(); let changed = false; Object.keys(this._auth_flows).forEach((flowId) => { const flow = this._auth_flows[flowId]; if (flow.auth_ttl) { if (now - flow.ts > flow.auth_ttl) { this.log.debug(`Deleted old flowId ${flow.username} ${flowId}`); delete this._auth_flows[flowId]; changed = true; } } else { if (now - flow.ts > TIMEOUT_PASSWORD_ENTER) { this.log.debug(`Deleted old flowId (no password) ${flowId}`); delete this._auth_flows[flowId]; changed = true; } } }); changed && this._saveAuth(); } /** * Generates entities from custom settings (TODO: name is misguiding!!) * * @returns resolves, when done. */ async _getManualEntities() { var _a, _b; try { const ids = []; for (const id of Object.keys(this._objectData.objects)) { const obj = this._objectData.objects[id]; if ((_b = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.custom) == null ? void 0 : _b[this.adapter.namespace]) { ids.push(id); } } ids.push(`${this.adapter.namespace}.control.alarm`); const created = []; for (const id of ids) { const entities = await this._processManualEntity(id); for (const entity of entities) { created.push(entity); entity.registerInCaches(); } } this._modules.entityRegistry.handleUpdatedEntities( created, false ); } catch (e) { this.adapter.log.error(`Could not get object view for getAllEntities: ${e.toString()} - ${e.stack}`); } } // ------------------------------- START OF CONVERTERS ---------------------------------------- // // Process manually created entity /** * Create manual entities from custom-part. Process one object here. * * @param id of ioBroker object * @returns manual entity */ async _processManualEntity(id) { var _a, _b, _c; try { const obj = (_a = this._objectData.objects[id]) != null ? _a : await this.adapter.getForeignObjectAsync(id); if (!obj) { return []; } if (!this._objectData.objects[id]) { this._objectData.objects[id] = obj; } if (id === `${this.adapter.namespace}.control.alarm`) { obj.common.custom = obj.common.custom || {}; obj.common.custom[this.adapter.namespace] = obj.common.custom[this.adapter.namespace] || {}; obj.common.custom[this.adapter.namespace].name = obj.common.custom[this.adapter.namespace].name || "defaultAlarm"; obj.common.custom[this.adapter.namespace].entity = "alarm_control_panel"; obj.common.custom[this.adapter.namespace].states = { state: id, arm_state: `${this.adapter.namespace}.control.alarm_arm_state` }; } else if (!((_c = (_b = obj.common) == null ? void 0 : _b.custom) == null ? void 0 : _c[this.adapter.namespace])) { return []; } const custom = obj.common.custom[this.adapter.namespace] || {}; const entityType = custom.entity || utils.autoDetermineEntityType(obj); const entity_id = utils.createEntityNameFromCustom(obj, this.adapter.namespace); const bridgeStates = (0, import_syntheticControl.syntheticControlStates)(entityType, custom); if (bridgeStates && Object.keys(bridgeStates).length > 0) { for (const stateId of Object.values(bridgeStates)) { if (stateId && !this._objectData.objects[stateId]) { try { this._objectData.objects[stateId] = await this.adapter.getForeignObjectAsync(stateId); } catch (e) { this.adapter.log.warn(`Could not get object ${stateId} for manual ${entityType}: ${e}`); } } } return (0, import_syntheticControl.buildManualViaConverter)({ entityType, id, custom, objects: this._objectData.objects, adapter: this.adapter, entityRegistry: this._modules.entityRegistry, forcedEntityId: entity_id }); } const entity = new import_baseEntity.BaseEntity(null, null, null, obj, entityType, entity_id); if (custom.attr_assumed_state && ["switch", "light", "cover", "climate", "fan", "humidifier", "group", "water_heater"].includes( entityType )) { entity.attributes.assumed_state = true; } entity.context.STATE = { getId: id, setId: id, attribute: "state" }; if (obj && obj.common && obj.common.states && ["string", "number"].includes(obj.common.type)) { entity.context.STATE.map2lovelace = obj.common.states; if (!(obj.common.states instanceof Array)) { entity.context.STATE.map2iob = {}; Object.keys(obj.common.states).forEach( (k) => entity.context.STATE.map2iob[obj.common.states[k]] = k ); } } entity.addID2entity(id); if (custom.states && custom.states.stateRead) { entity.context.STATE.getId = custom.states.stateRead; entity.addID2entity(custom.states.stateRead); } entity.isManual = true; if (custom.states) { if (custom.states.state && custom.states.state !== id) { this.log.error( `Please define custom settings on main object ${custom.states.state} and not on ${id}. Entity skipped` ); return []; } custom.states.state = id; for (const stateId of Object.values(custom.states)) { if (!this._objectData.objects[stateId]) { try { this._objectData.objects[stateId] = await this.adapter.getForeignObjectAsync(stateId); } catch (e) { this.adapter.log.warn( `Could not get object ${stateId} for manual entity ${entity_id} please check config in ${id}. Error: ${e}` ); } } } entity.fillFromStates(custom.states); } for (const key of Object.keys(custom)) { if (key.startsWith("attr_") && custom[key] !== "" && custom[key] !== void 0 && custom[key] !== null) { const attributeName = key.substring("attr_".length); entity.attributes[attributeName] = custom[key]; } } this.log.debug(`Create manual ${entityType} device: ${entity.entity_id} - ${id}`); if (entityType === "light") { return converterLight.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "input_datetime") { return converterDatetime.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "binary_sensor") { return converterBinarySensors.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "sensor") { return converterSensors.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "climate") { return converterClimate.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "geo_location") { return converterGeoLocation.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "device_tracker" || entityType === "person") { return converterDeviceTracker.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "camera") { entity.context.STATE = { getValue: "on", getId: null, attribute: "state" }; entity.context.ATTRIBUTES = [{ getId: id, attribute: "url" }]; entity.attributes.code_format = "number"; entity.attributes.access_token = import_node_crypto.default.createHmac( "sha256", (import_node_crypto.default.webcrypto.getRandomValues(new Uint32Array(1))[0] * 1e9).toString() ).update(Date.now().toString()).digest("hex"); entity.attributes.model_name = "Simulated URL"; entity.attributes.brand = "ioBroker"; entity.attributes.motion_detection = false; } else if (entityType === "alarm_control_panel") { return converterAlarmCP.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "input_number") { entity.attributes.min = obj.common.min !== void 0 ? obj.common.min : 0; entity.attributes.max = obj.common.max !== void 0 ? obj.common.max : 100; entity.attributes.step = obj.common.step || 1; entity.attributes.mode = entity.attributes.mode || obj.common.custom[this.adapter.namespace].mode || "slider"; const state = await this.adapter.getForeignStateAsync(id); entity.attributes.initial = state ? state.val || 0 : 0; } else if (entityType === "input_boolean") { const state = await this.adapter.getForeignStateAsync(id); entity.attributes.initial = (0, import_genericConverter.iobState2EntityState)(entity, state ? state.val : void 0, "initial"); } else if (entityType === "input_select") { return converterInputSelect.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "fan") { return convertFan.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "todo") { return this._modules.todo.processManualEntity( id, obj, entity, this._objectData.objects, custom ); } else if (entityType === "switch") { return converterSwitch.processManualEntity(id, obj, entity, this._objectData.objects, custom); } else if (entityType === "timer") { entity.context.STATE = { getId: null, setId: null, attribute: "state" }; entity.context.lastValue = null; entity.attributes.remaining = 0; entity.context.ATTRIBUTES = [ { attribute: "remaining", getId: id, setId: id, getParser: function(entity2, attr, state) { state = state || { val: null }; if (!state.val) { entity2.state = "idle"; } else if (entity2.context.lastValue === null) { entity2.state = "active"; } else if (entity2.context.lastValue === state.val) { entity2.state = "paused"; } else { entity2.state = "active"; } entity2.context.lastValue = state.val; if (typeof state.val === "string" && state.val.indexOf(":") !== -1) { entity2.attributes.remaining = state.val; } else { state.val = parseInt(state.val, 10); const hours = Math.floor(state.val / 3600); const minutes = Math.floor(state.val % 3600 / 60); const seconds = state.val % 60; entity2.attributes.remaining = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } } } ]; } entity.addID2entity(id); return [entity]; } catch (e) { this.adapter.log.error(`Could not process manual entity ${id}: ${e.toString()} - ${e.stack}`); } return []; } /** * Process a single service call from the frontend. * * @param ws websocket connection to the frontend * @param data data of the service call * @param entity_id entity id connected to the call. Required to be a single id in this function. * @returns resolves when done. */ async _processSingleCall(ws, data, entity_id) { var _a; const user = this._modules.person.getUserIDFromName((_a = ws.__auth) == null ? void 0 : _a.username); const entity = entityData.entityId2Entity[entity_id]; const id = entity.context.STATE.setId; if (entity.context.COMMANDS) { const command = entity.context.COMMANDS.find((c) => c.service === data.service); if (command && command.parseCommand) { return command.parseCommand(entity, command, data, user).then((result) => this._sendResponse(ws, data.id, result)).catch((e) => this._sendResponse(ws, data.id, { result: false, error: e.message || e })); } } if (data.service === "toggle") { this.log.debug(`toggle ${id}`); this.adapter.getForeignState( id, { user }, (err, state) => this.adapter.setForeignState( id, state ? !state.val : true, false, { user }, () => this._sendResponse(ws, data.id) ) ); } else if (data.service === "volume_set") { this.log.debug(`volume_set ${id}`); this.adapter.setForeignState( id, Number(data.service_data.value), false, { user }, () => this._sendResponse(ws, data.id) ); } else if (data.service === "trigger" || data.service === "turn_on" || data.service === "unlock" || data.service === "press") { this.log.debug(`${data.service} ${id}`); this.adapter.setForeignState(id, true, false, { user }, () => this._sendResponse(ws, data.id)); } else if (data.service === "turn_off" || data.service === "lock") { this.log.debug(`${data.service} ${id}`); this.adapter.setForeignState(id, false, false, { user }, () => this._sendResponse(ws, data.id)); } else if (data.service === "set_temperature") { this.log.debug(`set_temperature ${data.service_data.temperature}`); if (data.service_data.temperature !== void 0) { if (entity.context.ATTRIBUTES) { const attr = entity.context.ATTRIBUTES.find((attr2) => attr2.attribute === "temperature"); if (attr) { return this.adapter.setForeignState( attr.setId, Number(data.service_data.temperature), false, { user }, () => this._sendResponse(ws, data.id) ); } } } this.log.warn(`Cannot find attribute temperature in ${entity_id}`); this._sendResponse(ws, data.id); } else if (data.service === "set_operation_mode") { this.log.debug(`set_operation_mode ${data.service_data.operation_mode}`); this.adapter.setForeignState(id, false, false, { user }, () => this._sendResponse(ws, data.id)); } else if (data.service === "set_page") { this.log.debug(`set_page ${JSON.stringify(data.service_data.page)}`); if (typeof data.service_data.page === "object") { this.adapter.setState( "control.data", { val: data.service_data.page.title, ack: true }, () => { this.adapter.setState("control.command", { val: "changedView", ack: true }); } ); } } else if (data.service.startsWith("set_") && data.service !== "set_datetime") { this.log.debug(`${data.service}: ${id} = ${data.service_data[data.service.substring(4)]}`); let val = data.service_data[data.service.substring(4)]; if (!val && ["datetime"].includes(entity.context.type)) { val = data.service_data.datetime; } if (entity.context.STATE.map2iob) { val = Number(entity.context.STATE.map2iob[val]); if (!val && val !== 0) { val = data.service_data[data.service.substring(4)]; } } if (entity.context.stateType === "number") { val = Number(val); } this.adapter.setForeignState(id, val, false, { user }, () => this._sendResponse(ws, data.id)); } else if (data.service === "volume_mute") { this.log.debug(`volume_mute ${id} = ${data.service_data.is_volume_muted}`); this.adapter.setForeignState( id, data.service_data.is_volume_muted, false, { user }, () => this._sendResponse(ws, data.id) ); } else if (data.service.startsWith("select_")) { this.log.debug(`${data.service}: ${id} = ${data.service_data[data.service.substring(7)]}`); let val = data.service_data[data.service.substring(7)]; if (entity.context.STATE.map2iob) { val = Number(entity.context.STATE.map2iob[val]); if (!val && val !== 0) { val = data.service_data[data.service.substring(7)]; } } this.adapter.setForeignState(id, val, false, { user }, () => this._sendResponse(ws, data.id)); } else if (data.service.endsWith("_say")) { this.adapter.setForeignState(id, data.service_data.message, false, { user }, () => { this._sendResponse(ws, data.id); }); } else { this.log.warn(`Unknown service: ${data.service} (${JSON.stringify(data)})`); ws.send( JSON.stringify({ id, type: "result", success: false, error: { code: "not_found", message: "Service not found." } }) ); } } /** * Process service calls. Extracts entity_id from data. Can be an array, too. Also hands the call to modules, if * they process service calls. * * @param ws websocket connection to the frontend * @param data data of the service call * @returns resolves when done. */ async _processCall(ws, data) { var _a, _b; if (!data.service) { this.log.warn("Invalid service call. Make sure service looks like domain.service_name"); return; } if (data.domain === "system_log" && data.service === "write") { this.log.info(`Log from UI ${data.service_data.message}`); return this._sendResponse(ws, data.id); } let handled = false; for (const mod of Object.values(this._modules)) { handled = ((_b = await ((_a = mod.processServiceCall) == null ? void 0 : _a.call(mod, ws, data))) != null ? _b : false) || handled; } if (handled) { return; } if (data.target && data.target.entity_id) { data.service_data.entity_id = data.service_data.entity_id || data.target.entity_id; } let ids = [data.service_data.entity_id]; if (data.service_data.entity_id instanceof Array) { ids = data.service_data.entity_id; } delete data.service_data.entity_id; for (const id of ids) { if (!entityData.entityId2Entity[id]) { this.log.warn(`Unknown entity: ${id} for service call ${JSON.stringify(data)}`); } else { await this._processSingleCall(ws, data, id); } } } /** * Get states for all entities and fill entity states / attributes during startup. * * @returns resolves when all entity states have been populated */ async _getAllStates() { for (const entity of entityData.entities) { await this._getStatesForEntity(entity); } } /** * Read states from iobroker state database and fill entity states / attributes. * Usually done to read initial values. * * @param entity entity to get states for * @returns resolves when done. */ async _getStatesForEntity(entity) { entity.state = entity.state || "unknown"; if (entity.context.STATE && entity.context.STATE.getId) { try { const user = this.config.defaultUser; const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, { user }); if (state) { await this.onStateChange(entity.context.STATE.getId, state); } else { entity.state = "unknown"; entity.last_changed = Date.now() / 1e3; entity.last_updated = entity.last_changed; } } catch (e) { this.adapter.log.error(`Could not get state ${entity.context.STATE.getId}: ${e} - ${e.stack}`); } } else if (entity.context.type === "switch") { entity.state = "off"; } else if (entity.context.STATE.getValue !== void 0) { entity.state = entity.context.STATE.getValue; } else if (entity.context.type === "climate") { entity.state = "on"; } if (entity.context.ATTRIBUTES) { const ids = entity.context.ATTRIBUTES.map((entry) => entry.getId || ""); try { const states = await this.adapter.getForeignStatesAsync(ids); if (ids && ids.length) { entity.attributes = entity.attributes || {}; ids.forEach((id, i) => { const attribute = entity.context.ATTRIBUTES[i].attribute; if (attribute === "remaining" && entity.context.type === "timer") { if (!states[id].val) { entity.state = "idle"; } else { entity.state = "active"; } entity.context.lastValue = states[id].val; } else { void this.onStateChange(id, states[id]); } }); } } catch (e) { this.adapter.log.error(`Could not update state: ${e} - ${e.stack}`); } } } /** * Handle a state change. * * @param id id of the state * @param state new state object * @param forceUpdate force entity.state update of all clients * @returns resolves when done. */ async onStateChange(id, state, forceUpdate = false) { var _a; if (this._subscribedAll && !forceUpdate) { const relevant = !!entityData.iobID2entity[id] || id.startsWith(`${this.adapter.namespace}.`) || this._modules.template.referencesState(id, this._wss); if (!relevant) { return; } } if (state) { this._modules.themes.onStateChange(id, state); } const entities = entityData.iobID2entity[id]; if (entities) { entities.forEach((entity) => { if (!entity || !entity.context) { this.log.warn(`iobID2entity[${id}] contains an invalid entry - skipping.`); return; } let updated = false; if (state) { if (entity.context.STATE.getId === id) { updated = true; entity.updateTimestamp(state, true); if (entity.context.STATE.getParser) { entity.context.STATE.getParser(entity, "state", state); } else { entity.state = (0, import_genericConverter.iobState2EntityState)(entity, state.val); } } if (entity.context.ATTRIBUTES) { const attributes = entity.context.ATTRIBUTES.filter((e) => e.getId === id); for (const attr of attributes) { updated = true; entity.updateTimestamp(state, false); if (attr.getParser) { attr.getParser(entity, attr, state); } else { utils.setJsonAttribute( entity.attributes, attr.attribute, (0, import_genericConverter.iobState2EntityState)(entity, state.val, attr.attribute) ); } } } } if (!updated && !forceUpdate) { return; } this.updateEntityInFrontend(entity, state); }); } for (const mod of Object.values(this._modules)) { void ((_a = mod.onStateChange) == null ? void 0 : _a.call(mod, id, state, this._wss)); } } /** * Send entity update to all clients / frontends. * * @param entity entity that changed. * @param state ioBroker state, used to get the timestamp. */ updateEntityInFrontend(entity, state) { const t = { type: "event", event: { a: {}, event_type: "subscribe_entities", origin: "LOCAL", time_fired: state ? state.ts : entity.lu || entity.last_updated || Date.now() / 1e3 } }; t.event.a[entity.entity_id] = this._getShortEntity(entity); this._wss && this._wss.clients.forEach((ws) => { if (ws._subscribes && ws._subscribes.subscribe_entities) { ws._subscribes.subscribe_entities.forEach((id) => { t.id = id; ws.send(JSON.stringify(t)); }); } }); } // ------------------------------- END OF CONVERTERS ---------------------------------------- // /** * Process one ioBroker object, hand it to type-detector, create entities if devices are detected. * * @param ids ids of all ioBroker objects (or only alias.*) * @param objects all ioBroker objects in ids * @param id id of object to process * @param room room object assigned to object * @param func func object assigned to object * @param existingEntities array of created entities if any * @returns resolves when done. */ async _processIobState(ids, objects, id, room, func, existingEntities) { if (!id) { return; } if (!objects[id]) { return; } const friendlyName = utils.getSmartName(objects, id, this.lang); if (!friendlyName && !room && !func) { return; } try { const options = { objects, id, _keysOptional: ids, _usedIdsOptional: this._objectData.usedKeys }; delete this.detector.cache[id]; const controls = this.detector.detect(options); if (controls) { import_converter.Converter.convertAll(controls, { id, friendlyName, room, func, objects, existingEntities, adapter: this, entityRegistry: this._modules.entityRegistry }); } else { this.adapter.log.debug(`[Type-Detector] Nothing found for ${options.id}`); } } catch (e) { this.adapter.log.error(`[Type-Detector] Cannot process "${id}": ${e} stack: ${e.stack}`); throw e; } } /** * Something changed in system, for example system location, so we need to update constant entities, for example zone.home. */ _updateConstantEntities() { let entityHome = entityData.entityId2Entity["zone.home"]; if (!entityHome) { entityHome = { entity_id: "zone.home", state: "zoning", attributes: { hidden: true, radius: 10, friendly_name: "Home", icon: "mdi:home" }, context: { id: "system.config", // not sure this makes a lot of sense. But prevents crash in UI. STATE: {}, //prevent warning on getting history. type: "zone" } }; entityData.entities.push(entityHome); entityData.entityId2Entity[entityHome.entity_id] = entityHome; } entityHome.attributes.latitude = parseFloat(this.systemConfig.latitude); entityHome.attributes.longitude = parseFloat(this.systemConfig.longitude); entityHome.last_changed = (this.systemConfig.ts || Date.now()) / 1e3; entityHome.last_updated = (this.systemConfig.ts || Date.now()) / 1e3; this._modules.entityRegistry.handleUpdatedEntities([entityHome], false); this._updateSunEntity(); } /** * Create/refresh the synthetic `sun.sun` entity (Home Assistant style) from the configured * latitude/longitude using suncalc. The GPS position from system.config is enough to compute * everything; ioBroker exposes no astro API to adapters. Does nothing when no location is set. */ _updateSunEntity() { var _a, _b; const lat = parseFloat((_a = this.systemConfig) == null ? void 0 : _a.latitude); const lng = parseFloat((_b = this.systemConfig) == null ? void 0 : _b.longitude); if (isNaN(lat) || isNaN(lng)) { if (!this._sunLocationWarned) { this.log.info("No latitude/longitude in system.config - sun.sun entity is not created."); this._sunLocationWarned = true; } return; } const { state, attributes } = (0, import_sun.computeSunState)(lat, lng); let sun = entityData.entityId2Entity["sun.sun"]; const isNew = !sun; if (!sun) { sun = { entity_id: "sun.sun", state, attributes: { friendly_name: "Sun", icon: "mdi:white-balance-sunny" }, context: { id: "sun.sun", STATE: {}, type: "sun" } }; entityData.entities.push(sun); entityData.entityId2Entity["sun.sun"] = sun; this.log.debug(`Created sun.sun entity (lat ${lat}, lng ${lng}).`); } sun.state = state; Object.assign(sun.attributes, attributes); sun.last_changed = Date.now() / 1e3; sun.last_updated = Date.now() / 1e3; if (isNew) { this._modules.entityRegistry.handleUpdatedEntities([sun], false); } else { this.updateEntityInFrontend(sun); } } /** * Create one entity from type-detector * * @param id of the main object (i.e., device) * @returns array of created entities if any */ async _createOneDevice(id) { if (this.adapter.config.aliasOnly) { if (!id.startsWith("alias.0.")) { this.log.debug( `Object ${id} changed, update of automatic created entities not relevant for us, because out of alias.` ); return []; } } const foundRoom = utils.findEnumForId(Object.values(this._objectData.rooms), id); const foundFunc = utils.findEnumForId(Object.values(this._objectData.functions), id); if (foundRoom && foundFunc) { if (this._objectData.ids.length !== Object.keys(this._objectData.objects).length) { this._objectData.ids = Object.keys(this._objectData.objects); this._objectData.ids.sort(); } const entities = []; this.log.debug("Starting processIobState", foundRoom._id, foundFunc._id); await this._processIobState( this._objectData.ids, this._objectData.objects, id, foundRoom, foundFunc, entities ); this._objectData.usedKeys = []; this.log.debug(`Done processIobState, got ${entities.length} new entities.`); for (const entity of entities) { entity.unregister(); entity.registerInCaches(); } return entities; } return []; } /** * Update all devices from type-detector * * @returns array of entities created */ async _updateDevices() { const result = []; try { await this._readObjects(); if (this._objectData.ids.length !== Object.keys(this._objectData.objects).length) { this._objectData.ids = Object.keys(this._objectData.objects); this._objectData.ids.sort(); } for (const func of Object.values(this._objectData.functions)) { if (!func.common || !func.common.members || typeof func.common.members !== "object" || !func.common.members.length) { continue; } for (const id of func.common.members) { for (const room of Object.values(this._objectData.rooms)) { if (!room.common || !room.common.members || typeof func.common.members !== "object" || !room.common.members.length) { continue; } const pos = room.common.members.indexOf(id); if (pos !== -1) { await this._processIobState( this._objectData.ids, this._objectData.objects, id, room, func, result ); } } } } this._objectData.usedKeys = []; } catch (e) { this.adapter.log.error(`Could not create auto entities: ${e.stack}`); } result.forEach( (entity) => { var _a, _b; return this.adapter.log.debug(`AUTO Device detected: ${(_a = entity.context) == null ? void 0 : _a.id} => ${(_b = entity.context) == null ? void 0 : _b.type}`); } ); this.log.debug(`Found ${result.length} auto created entities.`); this._modules.entityRegistry.handleUpdatedEntities(result, false); return result; } /** * Read all objects from object database that are required to create entities from type-detector results. * * @returns all objects */ async _readObjects() { var _a; const objects = this._objectData.objects; if (Object.keys(this._objectData.objects).length < 10) { try { const params = {}; if (this.adapter.config.aliasOnly) { params.startkey = "alias.0."; params.endkey = "alias.0.\u9999"; } const _states = await this.adapter.getObjectViewAsync("system", "state", params); const _channels = await this.adapter.getObjectViewAsync("system", "channel", params); const _devices = await this.adapter.getObjectViewAsync("system", "device", params); const _folders = await this.adapter.getObjectViewAsync("system", "folder", {}); const _enums = await this.adapter.getObjectViewAsync("system", "enum", {}); if (_devices && _device