iobroker.lovelace
Version:
With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI
1,227 lines • 117 kB
JavaScript
"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