UNPKG

iobroker.lovelace

Version:

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

2,880 lines 143 kB
const fs                     = require('fs');
const crypto                 = require('crypto');
const WebSocket              = require('ws');
const bodyParser             = require('body-parser');
const SERVICES               = require('./services');
const PANELS                 = require('./panels');
const multer                 = require('multer');
const mime                   = require('mime');
const yaml                   = require('js-yaml');
const axios                  = require('axios');
const jstz                   = require('jstimezonedetect');
const utils                  = require('./converters/utils');
const processBlind           = require('./converters/cover').processBlind;
const converterSwitch        = require('./converters/switch');
const converterLight         = require('./converters/light');
const converterBinarySensors = require('./converters/binary_sensor');
const converterSensors       = require('./converters/sensor');
const processMediaPlayer     = require('./converters/media_player').processMediaPlayer;
const converterClimate       = require('./converters/climate');
const converterWeather       = require('./converters/weather');
const processImage           = require('./converters/camera').processImage;
const processLock            = require('./converters/lock').processLock;
const processLocation        = require('./converters/geo_location').processLocation;
const converterDatetime      = require('./converters/input_datetime');
const converterAlarmCP       = require('./converters/alarm_control_panel');
const converterInputSelect   = require('./converters/input_select');
const entityData             = require('./dataSingleton');

const bindings   = require('./bindings');

const TIMEOUT_PASSWORD_ENTER = 180000; // 3 min
const TIMEOUT_AUTH_CODE = 10000; // 10sec

const {Types, ChannelDetector} = require('iobroker.type-detector');
const ignoreIds = [
    /^system\./,
    /^script\./,
];
const express = require('express');

const ROOT_DIR = '../hass_frontend';

const VERSION  = '0.102.1';
const NO_TOKEN = 'no_token';

function getRootPath() {
    if (ROOT_DIR.match(/^\w:/) || ROOT_DIR.startsWith('/')) {
        return ROOT_DIR + '/';
    } else {
        return `${__dirname}/${ROOT_DIR}/`;
    }
}

const generateRandomToken = function (callback) {
    crypto.randomBytes(256, (ex, buffer) => {
        crypto.randomBytes(32, (ex, secret) => {
            if (ex) {
                return callback('server_error');
            }

            const token = crypto
                .createHmac('sha256', secret)
                .update(buffer)
                .digest('hex');

            callback(false, token);
        });
    });
};

/*
possible HASS entity types:
- fan
- input_boolean
- light => STATE on/off, attributes: [brightness, hs_color([h,s]), min_mireds, max_mireds, color_temp, white_value, effect_list, effect, supported_features ], commands: turn_on(brightness_pct, hs_color, brightness, color_temp, white_value, effect), turn_off, toggle
  supported_features: brightness=0x01, colorTemp=0x02, effectList=0x04, color=0x10, whiteValue=0x80
- switch => STATE on/off, attributes: [brightness, hs_color], commands: turn_on, turn_off, toggle
- group
- automation
- climate => STATE on/off, attributes: [current_temperature, operation_mode, operation_list, target_temp_step, target_temp_low, target_temp_high,min_temp, max_temp, temperature], commands:
- cover
- configurator
- input_select
- input_number
- input_text
- lock
- media_player =>
    STATE on/off/playing/paused/idle/standby/unknown,
    attributes: [media_content_type(music/game/music/tvshow/...), entity_picture(as cover), media_duration, supported_features, is_volume_muted, volume_level, media_duration, media_position, media_position_updated_at, media_title, media_artist, media_series_title, media_season, media_episode, app_name, source, source_list, sound_mode, sound_mode_list],
    commands: media_play_pause, media_next_track, media_play_pause, media_previous_track, volume_set(volume_level), turn_off, turn_on, volume_down, volume_mute(is_volume_muted), volume_up, select_source(source), select_sound_mode(sound_mode),
    features for supported_features: PAUSE 0x1, volume_set 0x4, volume_mute 0x8, media_previous_track 0x10, media_next_track 0x20, turn_on 0x80, turn_off 0x100, play_media 0x200, volume_down/volume_up 0x400, select_source 0x800, select_sound_mode (0x10000), play (0x4000)
- scene
- script
- timer => STATE idle/paused/active, attributes: [remaining]
- vacuum
- water_heater
- weblink
- camera
- history_graph
- input_datetime
- sun
- updater
- binary_sensor => STATE on/off
- geo_location => attributes: [latitude, longitude, passive, icon, radius, entity_picture, gps_accuracy, source]
- weather => STATE any-text(no icon)/clear-night/cloudy/fog/hail/lightning/lightning-rainy/partlycloudy/pouring/rainy/snowy/snowy-rainy/sunny/windy/windy-variant, attributes: [temperature, pressure, humidity, wind_speed, wind_bearing, forecast]
       forecast is an array with max 5 items [{datetime: something for new Date(aa), temperature, templow, condition(see STATE), precipitation}, {...}]
 */

class WebServer {
    constructor(options) {
        this._lovelaceConfig   = null;
        this._ressourceConfig  = []; //new place to store custom cards (modules) and stuff.
        this.adapter           = options.adapter;
        this.config            = this.adapter.config;
        this.log               = this.adapter.log;
        this.lang              = 'en';
        this.detector          = new ChannelDetector();
        this.config.ttl        = parseInt(this.config.ttl, 10) || 3600;
        this.words             = options.words || {};

        //setup entityData:
        entityData.adapter     = this.adapter;
        entityData.log         = this.adapter.log;
        entityData.words       = this.words;

        this._notifications    = [];
        this._subscribed       = [];
        this._server           = options.server;
        this._app              = options.app;
        this._auth_flows       = {};
        this.templateStates    = {};
        this._themes           = {}; //themes storage
        this._currentTheme     = this.config.defaultTheme || 'default';
        this._currentThemeDark = this.config.defaultThemeDark || 'default';
        this._darkMode         = false;

        //object data for updates:
        this._objectData = {
            objects:             {}, //id -> object storage
            ids:                 [], //array of object ids.
            rooms:               [],
            functions:           [],
            roomNames:           {}, //id -> name storage
            funcNames:           {},
            updatedObjects:      [], //id + object pairs on updates to handle burst updates after burst.
            usedKeys:            []  //temporary storage for used keys (type-detector)
        };

        this.converter = {
            [Types.socket]:                 converterSwitch.processSocket,
            [Types.light]:                  converterLight.processLight,
            [Types.dimmer]:                 converterLight.processLightAdvanced,
            [Types.ct]:                     converterLight.processLightAdvanced,
            [Types.hue]:                    converterLight.processLightAdvanced,
            [Types.rgb]:                    converterLight.processLightAdvanced,
            [Types.rgbSingle]:              converterLight.processLightAdvanced,
            [Types.motion]:                 converterBinarySensors.processMotion,
            [Types.window]:                 converterBinarySensors.processWindow,
            [Types.windowTilt]:             converterSensors.processWindowTilt,
            [Types.door]:                   converterBinarySensors.processDoor,
            [Types.button]:                 converterSwitch.processSocket,
            [Types.temperature]:            converterSensors.processTemperature,
            [Types.humidity]:               converterSensors.processHumidity,
            [Types.lock]:                   processLock,
            [Types.airCondition]:           converterClimate.processThermostatOrAirConditioning,
            [Types.thermostat]:             converterClimate.processThermostatOrAirConditioning,
            [Types.blind]:                  processBlind,
            [Types.blindButtons]:           processBlind,
            [Types.weatherForecast]:        converterWeather.processWeather,
            [Types.accuWeatherForecast]:    converterWeather.processAccuWeather,
            [Types.location]:               processLocation,
            [Types.location_one]:           processLocation,
            [Types.media]:                  processMediaPlayer,
            [Types.image]:                  processImage,
        };

        const concurrentPromises = [
            this.adapter.getForeignObjectAsync('system.config')
                .then(config => {
                    this.lang = config.common.language;
                    entityData.lang = this.lang;
                    this.systemConfig = config.common;
                    this._updateConstantEntities();

                    return this.adapter.getObjectAsync('configuration');
                })
                .then(config => {
                    if (config && config.native && config.native.title) {
                        this._lovelaceConfig = config.native;
                        this._lovelaceConfig.hideToolbar = this.config.hideHeader;
                    } else {
                        this._lovelaceConfig = require('./defaultConfig');
                    }

                    return this._readNotifications();
                }),
            this._readAllEntities(),
            this._listFiles(),
            this._initThemes()
        ];

        Promise.all(concurrentPromises).then(() => {
            this.adapter.subscribeObjects('configuration');
            this.adapter.subscribeStates('control.*');
            this.adapter.subscribeStates('notifications.*');
            this.adapter.subscribeStates('conversation');
            this._init();

            // check every minute
            if (this.config.auth !== false) {
                this._clearInterval = setInterval(() => this.clearAuth(), 60000);
            }
            this.log.debug('Initialization done.');
        });
    }

    async _readAllEntities() {
        const smartDevices = await this._updateDevices();
        for (const entity of smartDevices) {
            //fill entity into
            utils.fillEntityIntoCaches(entity);
        }
        await this._getAllEntities(); //creates manual entities.
        //now all entities are created. Check for icon urls:
        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();
        this._manageSubscribesFromConfig();
        await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
    }

    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();
    }

    async _getAllEntities() {
        try {
            const doc = await this.adapter.getObjectViewAsync('custom', 'state', {});
            const ids = [];
            if (doc && doc.rows) {
                for (let i = 0, l = doc.rows.length; i < l; i++) {
                    if (doc.rows[i].value) {
                        const id = doc.rows[i].id;
                        if (doc.rows[i].value[this.adapter.namespace]) {
                            ids.push(id);
                        }
                    }
                }
            }
            ids.push(this.adapter.namespace + '.control.alarm');

            for (const id of ids) {
                const entities = await this._processManualEntity(id);
                for (const entity of entities) {
                    utils.fillEntityIntoCaches(entity);
                }
            }
        } catch (e) {
            this.adapter.log.error(`Could not get object view for getAllEntities: ${e.toString()} - ${e.stack}`);
        }
    }

    // ------------------------------- START OF CONVERTERS ---------------------------------------- //

    _iobState2EntityState(id, val, type) {
        type = type || entityData.iobID2entity[id][0].context.type;
        const pos = type.lastIndexOf('.');
        if (pos !== -1) {
            type = type.substring(pos + 1);
        }

        if (type === 'light' || type === 'switch' || type=== 'input_boolean') {
            return val ? 'on' : 'off';
        } else
        if (type === 'binary_sensor') {
            return val ? 'on' : 'off';
        } else
        if (type === 'lock') {
            return val ? 'unlocked' : 'locked';
        } else
        if (typeof val === 'boolean') {
            return val ? 'on' : 'off';
        } else {
            return val === null || val === undefined ? 'unknown' : val;
        }
    }

    // Process manually created entity
    /**
     * Create manual entity from custom-part.
     * @param {string} id of ioBroker object
     * @param {string} [forcedEntityId] possible forced entity Id on conflict case.
     * @returns {Promise<{context: {id: string, type: string}, attributes: {friendly_name: string}, entity_id: string}[]|entity[]|*[]>}
     * @private
     */
    async _processManualEntity(id, forcedEntityId) {
        try {
            const obj = await this.adapter.getForeignObjectAsync(id);
            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'
                };
            }
            const custom = obj.common.custom[this.adapter.namespace] || {};
            const entityType = custom.entity || utils.autoDetermineEntityType(obj);
            const entity_id = forcedEntityId || utils.createEntityNameFromCuston(obj, this.adapter.namespace);

            const entity = utils.processCommon(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};
            utils.addID2entity(id, entity);
            if (custom.states && custom.states.stateRead) {
                entity.context.STATE.getId = custom.states.stateRead;
                utils.addID2entity(custom.states.stateRead, entity);
            }
            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;
                utils.fillEntityFromStates(custom.states, entity);
            }
            for (const key of Object.keys(custom)) {
                if (key.startsWith('attr_')) {
                    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 === 'camera') {
                entity.context.STATE = {getValue: 'on'};
                entity.context.ATTRIBUTES = [{getId: id, attribute: 'url'}];
                entity.attributes.code_format = 'number';
                entity.attributes.access_token = crypto
                    .createHmac('sha256', (Math.random() * 1000000000).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 !== undefined ? obj.common.min : 0;
                entity.attributes.max  = obj.common.max !== undefined ? 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'; //or box, will become input box.
                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 = this._iobState2EntityState(id, state ? state.val : undefined, entityType);
            }
            else if (entityType === 'input_select') {
                return converterInputSelect.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') {
                // - timer => STATE idle/paused/active, attributes: [remaining]
                entity.context.STATE = {getId: null, setId: null}; // will be simulated
                entity.context.lastValue = null;
                entity.attributes.remaining = 0;
                entity.context.ATTRIBUTES = [{
                    attribute: 'remaining',
                    getId: id,
                    setId: id,
                    getParser: function (entity, attr, state) {
                        state = state || {val: null};
                        // - timer => STATE idle/paused/active, attributes: [remaining]
                        // if 0 => timer is off
                        if (!state.val) {
                            entity.state = 'idle';
                        } else if (entity.context.lastValue === null) {
                            entity.state = 'active';
                        } else if (entity.context.lastValue === state.val) {
                            // pause
                            entity.state = 'paused';
                        } else {
                            // active
                            entity.state = 'active';
                        }

                        entity.context.lastValue = state.val;

                        // seconds to D HH:MM:SS
                        if (typeof state.val === 'string' && state.val.indexOf(':') !== -1) {
                            entity.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;
                            entity.attributes.remaining = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
                        }
                    }
                }];
            }

            utils.addID2entity(id, entity);

            return [entity];
        } catch (e) {
            this.adapter.log.error(`Could not process manual entity ${id}: ${e.toString()} - ${e.stack}`);
        }
    }

    async _processSingleCall(ws, data, entity_id) {
        const user = await this._getUserId(ws.__auth.username || this.config.defaultUser);

        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, data.service_data.value, false, {user}, () =>
                this._sendResponse(ws, data.id));
        } else
        if (data.service === 'trigger' || data.service === 'turn_on' || data.service === 'unlock') {
            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 !== undefined) {
                if (entity.context.ATTRIBUTES) {
                    const attr = entity.context.ATTRIBUTES.find(attr => attr.attribute === 'temperature');
                    if (attr) {
                        return this.adapter.setForeignState(attr.setId, 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.instance', {
                        val: self._instance,
                        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)]);
            // set_value          => service_data.value
            // set_operation_mode => service_data.operation_mode
            // set_temperature    => service_data.temperature
            // set_speed          => service_data.speed

            this.adapter.setForeignState(id, data.service_data[data.service.substring(4)], 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}`);
            // volume_mute        => 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)]}`);
            // select_option      => service_data.option
            // select_source      => service_data.source

            this.adapter.setForeignState(id, data.service_data[data.service.substring(7)], 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)})`);
            //{'id": 21, "type": "result", "success": false, "error": {"code": "not_found", "message": "Service not found."}}
            ws.send(JSON.stringify({id, type: 'result', success: false, error: {code: 'not_found', message: 'Service not found.'}}));
        }
    }

    async _processCall(ws, data) {
        if (!data.service) {
            this.log.warn('Invalid service call. Make sure service looks like domain.service_name');
            return;
        }

        if (data.service === 'dismiss') {
            this.log.debug('dismiss ' + data.service_data.notification_id);

            return this._clearNotification(data.service_data.notification_id).then(() =>
                this._sendResponse(ws, data.id));
        }

        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 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; //make sure we do not use entity_id array in processSingleCall -> use param entity_id there.

        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);
            }
        }
    }

    async _getAllStates() {
        let entity = entityData.entities.find(e => e.state === undefined);
        while (entity) {
            entity.state = 'unknown';
            if (entity.context.STATE && entity.context.STATE.getId) {
                try {
                    const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always defaultUser?
                    const state = await this.adapter.getForeignStateAsync(entity.context.STATE.getId, {user});
                    if (state) {
                        this.onStateChange(entity.context.STATE.getId, state);
                    } else {
                        entity.state = 'unknown';
                        try {
                            entity.last_changed = new Date().toISOString();
                        } catch (e) {
                            this.adapter.log.warn(`Invalid last changed for ${entity.context.STATE.getId}`);
                        }
                        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 !== undefined) {
                entity.state = entity.context.STATE.getValue;
            } else if (entity.context.type === 'climate') {
                entity.state = 'on';
            }

            //handle attributes:
            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 {
                                this.onStateChange(id, states[id]);
                            }
                        });
                    }
                } catch (e) {
                    this.adapter.log.error(`Could not update state: ${e} - ${e.stack}`);
                }
            }
            entity = entityData.entities.find(e => e.state === undefined);
        }
    }

    async onStateChange(id, state, forceUpdate = false) {
        if (state) {
            if (id === this.adapter.namespace + '.control.shopping_list') {
                return this._sendUpdate('shopping_list_updated');
            } else if (id === this.adapter.namespace + '.notifications.list') {
                if (!state.ack) {
                    await this._readNotifications();
                }
                return this._sendUpdate('persistent_notifications_updated');
            } else if (id === this.adapter.namespace + '.notifications.add') {
                return !state.ack &&
                    this.addNotification(state.val).then(() =>
                        this._sendUpdate('persistent_notifications_updated'));
            } else if (id === this.adapter.namespace + '.notifications.clear') {
                return !state.ack &&
                    this._clearNotification(state.val).then(() =>
                        this._sendUpdate('persistent_notifications_updated'));
            } else if (id === this.adapter.namespace + '.control.theme' || id === this.adapter.namespace + '.control.themeDark') {
                const dark = id.includes('Dark');
                if (this._themes[state.val] || state.val === 'default') {
                    this[dark ? '_currentThemeDark' : '_currentTheme'] = state.val;
                    this._sendUpdate('themes_updated');
                }
            } else if (id === this.adapter.namespace + '.control.darkMode') {
                if (this._darkMode !== state.val) {
                    this._darkMode = !!state.val;
                    this._sendUpdate('themes_updated');
                }
            } else if (id === this.adapter.namespace + '.conversation') {
                if (state.ack) {
                    // send answer to conversation dialog
                    this._wss && this._wss.clients.forEach(client => {
                        if (client.__conversations && client.readyState === WebSocket.OPEN) {

                            Object.keys(client.__conversations).forEach(conversation_id => {
                                client.__conversations[conversation_id].timer && clearTimeout(client.__conversations[conversation_id].timer);

                                const answer = {
                                    id: client.__conversations[conversation_id].id,
                                    type: 'result',
                                    success: true,
                                    result: {
                                        speech: {
                                            plain: {
                                                speech: state.val,
                                                extra_data: null
                                            }
                                        },
                                        card: {}
                                    }
                                };
                                client.send(JSON.stringify(answer));
                            });
                        }
                    });
                }
            }
        }

        const changedStates = {};
        this._wss && this._wss.clients.forEach(client => {
            if (client.__templates && client.readyState === WebSocket.OPEN) {
                client.__templates.forEach(t => {
                    if (t.ids.includes(id)) {
                        const _state = state || {val: null};
                        if (changedStates[id] || (this.templateStates[id] && this.templateStates[id].val !== _state.val)) {
                            this.templateStates[id] = _state;
                            changedStates[id] = true;
                            const event = {
                                id: t.id,
                                type: 'event',
                                event: {
                                    result: bindings.formatBinding(t.template, this.templateStates)
                                }
                            };
                            client.send(JSON.stringify(event));
                        }
                    }
                });
            }
        });

        const entities = entityData.iobID2entity[id];
        if (entities) {
            entities.forEach(entity => {
                let updated = false;
                if (state) {
                    // {id: 2, type: "event", "event": {"event_type": "state_changed", "data": {"entity_id": "sun.sun", "old_state": {"entity_id": "sun.sun", "state": "above_horizon", "attributes": {"next_dawn": "2019-05-17T02:57:08+00:00", "next_dusk": "2019-05-16T19:44:32+00:00", "next_midnight": "2019-05-16T23:21:40+00:00", "next_noon": "2019-05-17T11:21:38+00:00", "next_rising": "2019-05-17T03:36:58+00:00", "next_setting": "2019-05-16T19:04:54+00:00", "elevation": 54.81, "azimuth": 216.35, "friendly_name": "Sun"}, "last_changed": "2019-05-16T09:09:53.424242+00:00", "last_updated": "2019-05-16T12:46:30.001390+00:00", "context": {id: "05356b1a7df54b2f939d3c7f8a3e05b4", "parent_id": null, "user_id": null}}, "new_state": {"entity_id": "sun.sun", "state": "above_horizon", "attributes": {"next_dawn": "2019-05-17T02:57:08+00:00", "next_dusk": "2019-05-16T19:44:32+00:00", "next_midnight": "2019-05-16T23:21:40+00:00", "next_noon": "2019-05-17T11:21:38+00:00", "next_rising": "2019-05-17T03:36:58+00:00", "next_setting": "2019-05-16T19:04:54+00:00", "elevation": 54.71, "azimuth": 216.72, "friendly_name": "Sun"}, "last_changed": "2019-05-16T09:09:53.424242+00:00", "last_updated": "2019-05-16T12:47:30.000414+00:00", "context": {id: "e738dc26af1d48b4964c6d9805179595", "parent_id": null, "user_id": null}}}, "origin": "LOCAL", "time_fired": "2019-05-16T12:47:30.000414+00:00", "context": {id: "e738dc26af1d48b4964c6d9805179595", "parent_id": null, "user_id": null}}}
                    if (entity.context.STATE.getId === id) {
                        updated = true;
                        try {
                            entity.last_changed = new Date(state.lc).toISOString();
                        } catch (e) {
                            this.adapter.log.warn(`Invalid last changed for ${entity.context.STATE.getId}`);
                        }
                        try {
                            entity.last_updated = new Date(state.ts).toISOString();
                        } catch (e) {
                            this.adapter.log.warn(`Invalid timestamp for ${entity.context.STATE.getId}`);
                        }

                        if (entity.context.STATE.getParser) {
                            entity.context.STATE.getParser(entity, 'state', state);
                        } else {
                            entity.state = this._iobState2EntityState(id, state.val);
                        }
                    }

                    //can have identical id for state and attributes.
                    if (entity.context.ATTRIBUTES) {
                        const attributes = entity.context.ATTRIBUTES.filter(e => e.getId === id);
                        for (const attr of attributes) {
                            updated = true;
                            try {
                                entity.last_changed = new Date(state.lc).toISOString();
                            } catch (e) {
                                this.adapter.log.warn(`Invalid last changed for ${attr.getId}`);
                            }
                            try {
                                entity.last_updated = new Date(state.ts).toISOString();
                            } catch (e) {
                                this.adapter.log.warn(`Invalid timestamp for ${attr.getId}`);
                            }

                            if (attr.getParser) {
                                attr.getParser(entity, attr, state);
                            } else {
                                utils.setJsonAttribute(entity.attributes, attr.attribute, this._iobState2EntityState(null, state.val, attr.attribute), this.log);
                            }
                        }
                    }
                }

                if (!updated && !forceUpdate) {
                    return; //nothing happened -> do not notify UI.
                }

                let time_fired;

                try {
                    time_fired = new Date(state ? state.ts : undefined).toISOString();
                } catch (e) {
                    time_fired = new Date().toISOString();
                }
                const t = {
                    type: 'event',
                    event: {
                        event_type: 'state_changed',
                        data: {
                            entity_id: entity.entity_id,
                            new_state: entity
                        },
                        origin: 'LOCAL',
                        time_fired
                    }
                };

                this._wss && this._wss.clients.forEach(ws => {
                    if (ws._subscribes && ws._subscribes.state_changed) {
                        ws._subscribes.state_changed.forEach(id => {
                            t.id = id;
                            ws.send(JSON.stringify(t));
                        });
                    }
                });

            });
        }
    }

    // ------------------------------- END OF CONVERTERS ---------------------------------------- //

    async _processIobState(ids, objects, id, roomName, funcName, result, forcedEntityId) {
        if (!id) {
            return;
        }

        // object might be deleted but still in room/func enums.
        if (!objects[id]) {
            return;
        }

        let friendlyName = utils.getSmartName(objects, id);
        if (typeof friendlyName === 'object' && friendlyName) {
            friendlyName = friendlyName[this.lang] || friendlyName.en;
        }

        if (!friendlyName && !roomName && !funcName) {
            return;
        }

        try {
            // try to detect device
            const options = {
                objects:            objects,
                id:                 id,
                _keysOptional:      ids,
                _usedIdsOptional:   this._objectData.usedKeys
            };
            delete this.detector.cache[id];
            const controls = this.detector.detect(options);
            if (controls) {
                for (const control of controls) {
                    if (this.converter[control.type]) {
                        const entities = await this.converter[control.type](id, control, friendlyName, roomName, funcName, objects[id], objects, forcedEntityId);
                        // converter could return one ore more devices as array
                        if (entities && entities.length) {
                            //try to create battery_alarm:
                            const mainEntity = entities.find(x => x && x.entity_id);
                            if (mainEntity) {
                                //make battery have sensible entity id and make sure it is different from "host" device:
                                entities.push(converterBinarySensors.processBattery.call(this, control, friendlyName, roomName, funcName, objects, 'binary_sensor.' + mainEntity.entity_id.split('.')[1] + '_BatteryWarning'));
                            }

                            // iterate through entities
                            for (const entity of entities) {
                                if (!entity) continue;
                                if (!entity.context.iobType) {
                                    entity.context.iobType = control.type; //remember type.
                                }

                                const _entity = result.find(e => e.entity_id === entity.entity_id);
                                if (_entity) {
                                    this.log.debug(`Duplicates found for ${entity.entity_id}`);
                                    continue;
                                }

                                result.push(entity);

                                this.adapter.log.debug(`[Type-Detector] Created auto device: ${entity.entity_id} - ${control.type} - ${id}`);
                            }
                        }
                    } else {
                        this.adapter.log.debug(`[Type-Detector] device ${control.states.find(e => e.id).id} - ${control.type} - ${id} is not yet supported`);
                    }
                }
            } else {
                console.log(`[Type-Detector] Nothing found for ${options.id}`);
            }
        } catch (e) {
            this.adapter.log.error(`[Type-Detector] Cannot process "${id}": ${e} stack: ${e.stack}`);
            throw e;
        }
    }

    _updateConstantEntities() {
        //zone.home:
        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.
                }
            };
            entityData.entities.push(entityHome);
            entityData.entityId2Entity[entityHome.entity_id] = entityHome;
        }
        const now = new Date().toISOString();
        entityHome.attributes.latitude = parseFloat(this.systemConfig.latitude);
        entityHome.attributes.longitude = parseFloat(this.systemConfig.longitude);
        entityHome.last_changed = now;
        entityHome.last_updated = now;
    }

    /**
     * Create one entity from type-detector
     * @param {string} id of main object (i.e. device)
     * @param {string} [forcedEntityId] possible forced entity_id on conflict cases.
     * @returns {Promise<*[]>}
     * @private
     */
    async _createOneDevice(id, forcedEntityId) {
        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();
            }

            let roomName = this._objectData.roomNames[foundRoom._id];
            let funcName = this._objectData.funcNames[foundFunc._id];
            if (!roomName) {
                roomName = utils.getEnumName(foundRoom, this.lang);
                this._objectData.roomNames[foundRoom._id] = roomName;
            }
            if (!funcName) {
                funcName = utils.getEnumName(foundFunc, this.lang);
                this._objectData.roomNames[foundFunc._id] = funcName;
            }

            const entities = [];
            this.log.debug('Starting processIobState');
            await this._processIobState(this._objectData.ids, this._objectData.objects, id, roomName,
                funcName, entities, forcedEntityId);
            this._objectData.usedKeys = []; //clean this up again.
            this.log.debug('Done processIobState');

            if (entities.length) {
                for (const entity of entities) {
                    this.log.debug(`Object ${id} changed, updated entity ${entity.entity_id}.`);
                    //entityData.entities and entityData.entityId2Entity is already updated / overwritten in processCommon!
                    //update _ID2entity:
                    entityData.iobID2entity[id] = entityData.iobID2entity[id] || []; //make sure it exists.
                }
                this.log.debug(`Object ${id} did change, got ${entities.length} updated entities.`);
            } else {
                this.log.debug(`Object ${id} did change, got no updated entities.`);
            }
            return entities;
        }
        return [];
    }

    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();
            }

            this._objectData.roomNames = {}; //reset names on full update.
            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;

                // Get the name of function (with language and if name is empty)
                const funcName = utils.getEnumName(func, this.lang);
                this._objectData.funcNames[func._id] = funcName; //store for later updates.

                //for each member of func, look for same id in all rooms:
                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;

                        // find name for room if not found earlier
                        if (!this._objectData.roomNames[room._id]) {
                            // Get the name of function (with language and if name is empty)
                            const roomName = utils.getEnumName(room, this.lang);
                            this._objectData.roomNames[room._id] = roomName;
                        }

                        // If state or channel is in some room and in some function
                        const pos = room.common.members.indexOf(id);
                        if (pos !== -1) {
                            await this._processIobState(this._objectData.ids, this._objectData.objects, id, this._objectData.roomNames[room._id], funcName, result);
                        }
                    }
                }
            }

            this._objectData.usedKeys = [];
        } catch (e) {
            this.adapter.log.error('Could not create auto entities: ' + e.stack);
        }

        result.forEach(entity => this.adapter.log.debug(`AUTO Device detected: ${entity.context.id} => ${entity.context.type}`));
        this.log.debug(`Found ${result.length} auto created entities.`);
        return result;
    }

    async _readObjects() {
        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 _enums = await this.adapter.getObjectViewAsync('system', 'enum', {});
                if (_devices && _devices.rows) {
                    for (let i = 0; i < _devices.rows.length; i++) {
                        if (_devices.rows[i].value && _devices.rows[i].value._id && !ignoreIds.find(reg => reg.test(_devices.rows[i].value._id))) {
                            objects[_devices.rows[i].value._id] = _devices.rows[i].value;
                        }
                    }
                }
                if (_channels && _channels.rows) {
                    for (let i = 0; i < _channels.rows.length; i++) {
                        if (_channels.rows[i].value && _channels.rows[i].value._id && !ignoreIds.find(reg => reg.test(_channels.rows[i].value._id))) {
                            objects[_channels.rows[i].value._id] = _channels.rows[i].value;
                        }
                    }
                }
                if (_states && _states.rows) {
                    for (let i = 0; i < _states.rows.length; i++) {
                        if (_states.rows[i].value && _states.rows[i].value._id && !ignoreIds.find(reg => reg.test(_states.rows[i].value._id))) {
                            objects[_states.rows[i].value._id] = _states.rows[i].value;
                        }
                    }
                }
                if (_enums && _enums.rows) {
                    for (let i = 0; i < _enums.rows.length; i++) {
                        if (_enums.rows[i].value && _enums.rows[i].value._id) {
                            objects[_enums.rows[i].value._id] = _enums.rows[i].value;

                            if (_enums.rows[i].value._id.startsWith('enum.rooms.')) {
                                this._objectData.rooms[_enums.rows[i].value._id] = _enums.rows[i].value;
                            }
                            if (_enums.rows[i].value._id.startsWith('enum.functions.')) {
                                this._objectData.functions[_enums.rows[i].value._id] = _enums.rows[i].value;
                            }
                        }
                    }
                }

            } catch (e) {
                this.adapter.log.error(`Failed to get states / channels / devices / enums, entity generation won't work: ${e.toString()} - ${e.stack}`);
            }
        }
        return objects;
    }

    // set as resources the custom cards
    async _listFiles() {
        try {
            const list = await this.adapter.readDirAsync(this.adapter.namespace, '/cards/');
            if (list.length) {
                this._ressourceConfig = list.map(file => {
                    const pos = file.file.lastIndexOf('.');
                    const type = file.file.substring(pos + 1).toLowerCase();
                    this.log.debug(`Add custom cards: ${file.file} as ${type}`);
                    return {
                        type: type === 'js' ? 'module' : type,
                        url: '/cards/' + file.file
                    };
                });
            }
        } catch (err) {
            this.log.warn('Could not load custom cards: ' + err);
        }
    }

    _flatJSON(obj, entities) {
        Object.keys(obj).forEach(key => {
            if (obj[key] instanceof Object) {
                if (obj[key] instanceof Array && key === 'entities' && typeof obj[key][0] === 'string') {
                    //handle entities array -> custom:mini-graph-card stores entities just as string!
                    for (const entity of obj[key]) {
                        entities.indexOf(entity) === -1 && entities.push(entity);
                    }
                } else {
                    this._flatJSON(obj[key], entities);
                }
            } else {
                if (key === 'entity') {
                    entities.indexOf(obj[key]) === -1 && entities.push(obj[key]);
                }
            }
        });
    }

    _manageSubscribesFromConfig() {
        const entities = [];

        if (this._lovelaceConfig.views) {
            // iterate trough all objects to get all nested entities Ids
            this._flatJSON(this._lovelaceConfig.views, entities);
        }

        // this._lovelaceConfig.views && this._lovelaceConfig.views.forEach(view =>
        //     view.cards && view.cards.forEach(card => {
        //         card.entity && entities.indexOf(card.entity) === -1 && entities.push(card.entity);
        //         if (card.entities) {
        //             if (card.entities instanceof Array) {
        //                 card.entities.forEach(subCard => subCard.entity && entities.indexOf(subCard.entity) === -1 && entities.push(subCard.entity));
        //             } else
        //             if (typeof card.entities === 'string') {
        //                 entities.indexOf(card.entities) === -1 && entities.push(card.entities);
        //             }
        //         }
        //     }));

        let ids = [];
        entities.forEach(entityId => {
            const entity = entityData.entityId2Entity[entityId];
            if (!entity) {
                return;
            }
            if (entity.context.STATE && entity.context.STATE.getId && ids.indexOf(entity.context.STATE.getId) === -1) {
                ids.push(entity.context.STATE.getId);
            }
            if (entity.context.ATTRIBUTES) {
                entity.context.ATTRIBUTES.forEach(attr => {
                    if (attr.getId && ids.indexOf(attr.getId) === -1) {
                        ids.push(attr.getId);
                    }
                });
            }
        });

        // check all sockets
        this._wss && this._wss.clients.forEach(client =>
            client.__templates && client.__templates.forEach(t => ids = ids.concat(t.ids)));

        const deleted = this._subscribed.filter(id => ids.indexOf(id) === -1);

        // unsubscribe
        deleted.forEach(id => {
            this.log.debug('IoB UnSubscribe on ' + id);
            this.adapter.unsubscribeForeignStates(id);
            const pos = this._subscribed.indexOf(id);
            this._subscribed.splice(pos, 1);
        });

        // subscribe
        ids.forEach(id => {
            if (this._subscribed.indexOf(id) === -1) {
                this._subscribed.push(id);
                this.adapter.subscribeForeignStates(id);
                this.log.debug('IoB Subscribe on ' + id);
            }
        });
    }

    _renderIndex() {
        if (this._indexHtml) {
            return this._indexHtml;
        }
        const text = fs.readFileSync(getRootPath() + 'index.html').toString('utf-8');
        const lines = text.split('\n');
        const nLines = [];
        let template = false;
        let style = false;
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].trim().match(/^{%/)) {
                template = !template;
                continue;
            } else
            if (!style && lines[i].trim().match(/<style>/)) {
                style = true;
                nLines.push(lines[i]);
                nLines.push(fs.readFileSync(__dirname + '/../assets/style.css').toString('utf-8'));
                continue;
            } else
            if (lines[i].trim().match(/<\/body>/)) {
                const hideScript = [];
                /*if (this.config.hideHeader) {
                    hideScript.push('hideHeader();');
                }*/
                if (this.config.auth === false) {
                    hideScript.push('window.hassNoAuth = "NO_AUTH";');
                    hideScript.push('window.__tokenCache = {tokens: undefined, writeEnabled: false};');
                }
                nLines.push('<script>\n' + fs.readFileSync(__dirname + '/../assets/index.js').toString('utf-8') + hideScript.join('\n') + '\n</script>');
            }
            if (template) {
                continue;
            }
            nLines.push(lines[i]);
        }
        this._indexHtml = nLines.join('\n');
        this._indexHtml = this._indexHtml.replace(/{{ no_auth }}/g, '1'); // deprecated
        this._indexHtml = this._indexHtml.replace(/{{ use_oauth }}/g, '0');  // deprecated
        this._indexHtml = this._indexHtml.replace(/{{ theme_color }}/g, this._renderManifest().theme_color);  // deprecated
        this._indexHtml = this._indexHtml.replace(/#THEMEC/g, this._renderManifest().theme_color);
        return this._indexHtml;
    }

    _renderManifest() {
        let lang = 'en-US';
        if (this.lang === 'ru') {
            lang = 'ru-RU';
        } else
        if (this.lang === 'de') {
            lang = 'de-DE';
        }

        return {
            background_color: '#FFFFFF',
            description: 'Open-source home automation platform running on Node.js.',
            dir: 'ltr',
            display: 'standalone',
            icons: [
                {
                    sizes: '192x192',
                    src: '/static/icons/favicon-192x192.png',
                    type: 'image/png'
                },
                {
                    sizes: '384x384',
                    src: '/static/icons/favicon-384x384.png',
                    type: 'image/png'
                },
                {
                    sizes: '512x512',
                    src: '/static/icons/favicon-512x512.png',
                    type: 'image/png'
                },
                {
                    sizes: '1024x1024',
                    src: '/static/icons/favicon-1024x1024.png',
                    type: 'image/png'
                }
            ],
            lang,
            name: 'ioBroker',
            short_name: 'IoB',
            start_url: '/?homescreen=1',
            theme_color: '#03A9F4'
        };
    }

    _renderAuthorize() {
        let html = fs.readFileSync(getRootPath() + 'authorize.html').toString('utf-8');
        html = html.replace(/Home Assistant/g, 'ioBroker');
        return html;
    }

    _getConfig() {
        const tzone = jstz.determine().name();
        //this.log.debug("tz:" + tzone);
        return {
            latitude: parseFloat(this.systemConfig.latitude),
            longitude: parseFloat(this.systemConfig.longitude),
            elevation: 0,
            unit_system: {
                length: 'km',
                mass: 'g',
                pressure: 'mmHg',
                temperature: '\u00b0C',
                volume: 'L'
            },
            language: this.systemConfig.language,
            location_name: 'Home',
            time_zone: tzone,
            components: [
                'lovelace', 'conversation', 'history', 'shopping_list'
            ],
            version: VERSION
        };
    }

    _getThemes() {
        return {
            themes: this._themes,
            default_theme: this._currentTheme || this.config.defaultTheme || 'default',
            default_dark_theme: this._currentThemeDark || this.config.defaultThemeDark || 'default',
            darkMode: this._darkMode
        };
    }

    getHassStates() {
        // parse config for entity_ids
        return entityData.entities;

        /*return [
            {
                "entity_id": "sun.sun",
                "state": "above_horizon",
                "attributes": {
                    "next_dawn": "2019-05-17T02:57:08+00:00",
                    "next_dusk": "2019-05-16T19:44:32+00:00",
                    "next_midnight": "2019-05-16T23:21:40+00:00",
                    "next_noon": "2019-05-17T11:21:38+00:00",
                    "next_rising": "2019-05-17T03:36:58+00:00",
                    "next_setting": "2019-05-16T19:04:54+00:00",
                    "elevation": 54.81,
                    "azimuth": 216.35,
                    "friendly_name": "Sun"
                },
                "last_changed": "2019-05-16T09:09:53.424242+00:00",
                "last_updated": "2019-05-16T12:46:30.001390+00:00",
                "context": {id: "05356b1a7df54b2f939d3c7f8a3e05b4", "parent_id": null, "user_id": null}
            },
            {
                "entity_id": "zone.home",
                "state": "zoning",
                "attributes": {
                    "hidden": true,
                    "latitude": 50.1155,
                    "longitude": 8.6842,
                    "radius": 100,
                    "friendly_name": "Home",
                    "icon": "mdi:home"
                },
                "last_changed": "2019-05-16T09:09:53.425218+00:00",
                "last_updated": "2019-05-16T09:09:53.425218+00:00",
                "context": {id: "42760245d5a84438a4e2989bd39d31ef", "parent_id": null, "user_id": null}
            },
            {
                "entity_id": "sensor.yr_symbol",
                "state": "unknown",
                "attributes": {
                    "attribution": "Weather forecast from met.no, delivered by the Norwegian Meteorological Institute.",
                    "friendly_name": "yr Symbol",
                    "entity_picture": "https://api.met.no/weatherapi/weathericon/1.1/?symbol=None;content_type=image/png"
                },
                "last_changed": "2019-05-16T09:10:06.702562+00:00",
                "last_updated": "2019-05-16T09:10:06.702562+00:00",
                "context": {id: "ce038a425b484d27945e375ca2145ce9", "parent_id": null, "user_id": null}
            },
            {
                "entity_id": "group.all_devices",
                "state": "unknown",
                "attributes": {
                    "entity_id": [],
                    "order": 0,
                    "auto": true,
                    "friendly_name": "all devices",
                    "hidden": true
                },
                "last_changed": "2019-05-16T09:10:06.710375+00:00",
                "last_updated": "2019-05-16T09:10:06.710375+00:00",
                "context": {id: "ec4352089b944195a6b2de382fbd5b02", "parent_id": null, "user_id": null}
            },
            {
                "entity_id": "person.admin",
                "state": "unknown",
                "attributes": {
                    "editable": true,
                    id: "c110b265fd8b460d89f8b184e39c622c",
                    "user_id": "4e645b6d65ea4473b31a69d685781a17",
                    "friendly_name": "Admin"
                },
                "last_changed": "2019-05-16T09:10:07.335375+00:00",
                "last_updated": "2019-05-16T09:10:07.769945+00:00",
                "context": {id: "d9cf0d3c16974a04a1363e9c726cef55", "parent_id": null, "user_id": null}
            }
        ];*/
    }

    async onCards(req, res) {
        let file = req.url;
        const pos = req.url.indexOf('?');
        if (pos !== -1) {
            file = file.substring(0, pos);
        }
        try {
            const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
            const data = await this.adapter.readFileAsync(this.adapter.namespace, file, {user});
            const pos = file.lastIndexOf('.');
            res.setHeader('content-type', (mime.getType || mime.lookup).call(data.mimeType, file.substring(pos + 1).toLowerCase()));
            res.send(data.file);
        } catch (err) {
            this.log.warn(`Could not read card ${file}: ${err}`);
            res.status(404).send('File not found');
        }
    }

    async onCustomUI(req, res) {
        let file = req.url;
        const pos = req.url.indexOf('?');
        if (pos !== -1) {
            file = file.substring(0, pos);
        }
        try {
            const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
            const data = await this.adapter.readFileAsync(this.adapter.namespace, file.replace('/local/custom_ui/', '/cards/'), {user});
            const pos = req.url.lastIndexOf('.');
            res.setHeader('content-type', (mime.getType || mime.lookup).call(data.mimeType, file.substring(pos + 1).toLowerCase()));
            res.send(data.file);
        } catch (err) {
            this.log.warn(`Could not read customUI ${file}: ${err}`);
            res.status(404).send('File not found');
        }
    }

    /**
     * convert .ts of state e to ISOString with try/catch and now as fallback.
     * @param {ioBroker.state} e
     * @returns {string}
     * @private
     */
    _convertStateTStoISOString(e) {
        try {
            return new Date(e.ts).toISOString();
        } catch (error) {
            return new Date().toISOString();
        }
    }

    onAuth(req, res) {
        const now = Date.now();
        if (req.body.action === 'revoke') {
            const flowId = Object.keys(this._auth_flows)
                .find(flowId => this._auth_flows[flowId].refresh_token === req.body.refresh_token);
            if (flowId) {
                delete this._auth_flows[flowId];
                res.status(200).json({result: 'revoked'});
            } else {
                res.status(404).json({result: 'not exists'});
            }
        } else
        if (req.body.grant_type === 'authorization_code') {
            // simulate token exchange
            if (this.config.auth === false && req.body.code === 'NO_AUTH') {
                res.json({
                    access_token:  NO_TOKEN,
                    expires_in:    Math.round(3600000000),
                    refresh_token: NO_TOKEN,
                    token_type:   'Bearer'
                });
                return;
            }

            const flowId = Object.keys(this._auth_flows)
                .find(flowId => this._auth_flows[flowId].auth_code === req.body.code && now - this._auth_flows[flowId].ts < TIMEOUT_AUTH_CODE);

            if (!flowId) {
                res.status(403).json({error: 'auth code is invalid or expired'});
                //res.json({ok: false, status: 403, error: 'auth code is invalid or expired'});
            } else {
                generateRandomToken((_err, access_token) => {
                    generateRandomToken((_err, refresh_token) => {
                        console.log('generate new access token ' + JSON.stringify(req.body));

                        const flow = this._auth_flows[flowId];
                        //access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI5Yjc5Y2Q2OTZmN2U0ZDc2OTg1NGU4NGQ3YmY4NzNlMyIsImlhdCI6MTU1ODAwOTE1MywiZXhwIjoxNTU4MDEwOTUzfQ.QL9qA0QeOxfMoxPUfxH1kEZDGnDNlSHbQeMF8z2xLw0',
                        flow.access_token  = access_token;
                        flow.refresh_token = refresh_token;
                        flow.auth_ttl      = this.config.ttl * 1000;
                        flow.ts            = now;

                        res.json({
                            access_token:  flow.access_token,
                            expires_in:    Math.round(flow.auth_ttl / 1000),
                            refresh_token: flow.refresh_token,
                            token_type:    'Bearer'
                        });
                        this._saveAuth();
                    });
                });
            }
        } else if (req.body.grant_type === 'refresh_token') {
            const flowId = Object.keys(this._auth_flows)
                .find(flowId =>
                    this._auth_flows[flowId].refresh_token === req.body.refresh_token &&
                    now - this._auth_flows[flowId].ts < this._auth_flows[flowId].auth_ttl);
            if (!flowId) {
                res.status(403).json({error: 'refresh_token is invalid or expired'});
                //res.json({ok: false, status: 403, error: 'refresh_token is invalid or expired'});
            } else {
                generateRandomToken((_err, access_token) => {
                    generateRandomToken((_err, refresh_token) => {
                        console.log('generate access token' + JSON.stringify(req.body));

                        const flow = this._auth_flows[flowId];
                        //access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI5Yjc5Y2Q2OTZmN2U0ZDc2OTg1NGU4NGQ3YmY4NzNlMyIsImlhdCI6MTU1ODAwOTE1MywiZXhwIjoxNTU4MDEwOTUzfQ.QL9qA0QeOxfMoxPUfxH1kEZDGnDNlSHbQeMF8z2xLw0',
                        flow.access_token  = access_token;
                        flow.refresh_token = refresh_token;
                        flow.auth_ttl      = this.config.ttl * 1000;
                        flow.ts            = now;

                        res.json({
                            access_token:  flow.access_token,
                            expires_in:    Math.round(flow.auth_ttl / 1000),
                            refresh_token: flow.refresh_token,
                            token_type:    'Bearer'
                        });
                        this._saveAuth();
                    });
                });
            }
        }
    }

    async _initThemes() {
        //setup theme selection button:
        try {
            try {
                this._themes = yaml.safeLoad(this.config.themes || '') || {};
            } catch (depError) {
                if (depError.message.includes('yaml.safeLoad') && depError.message.includes('removed')) {
                    this._themes = yaml.load(this.config.themes || '') || {};
                } else {
                    throw depError;
                }
            }
        } catch (e) {
            this.log.error(`Cannot parse themes: ${e}`);
            this._themes = {};
        }
        const states = {'default': 'default'};
        for (const themeName of Object.keys(this._themes)) {
            states[themeName] = themeName;
        }
        await this.adapter.extendObjectAsync(this.adapter.namespace + '.control.theme', {common: {states}});
        await this.adapter.extendObjectAsync(this.adapter.namespace + '.control.themeDark', { common: {states}});

        const state = await this.adapter.getStateAsync(this.adapter.namespace + '.control.theme');
        //remember currently selected theme, if valid. Select default otherwise.
        if (state && (this._themes[state.val] || state.val === 'default')) {
            this._currentTheme = state.val;
        } else {
            this._currentTheme = this.config.defaultTheme || 'default';
            await this.adapter.setStateAsync(this.adapter.namespace + '.control.theme', this._currentTheme, true);
        }
        const darkSate = await this.adapter.getStateAsync(this.adapter.namespace + '.control.themeDark');
        //remember currently selected theme, if valid. Select default otherwise.
        if (darkSate && (this._themes[darkSate.val] || darkSate.val === 'default')) {
            this._currentThemeDark = darkSate.val;
        } else {
            this._currentThemeDark = this.config.defaultThemeDark || 'default';
            await this.adapter.setStateAsync(this.adapter.namespace + '.control.themeDark', this._currentThemeDark, true);
        }

        const darkModeState = await this.adapter.getStateAsync(this.adapter.namespace + '.control.darkMode');
        if (darkModeState) {
            this._darkMode = !!darkModeState.val;
        }
    }

    _init() {
        const upload = multer();

        this.adapter.subscribeForeignObjects('*');

        this._app.use(bodyParser.json());
        this._app.use(bodyParser.urlencoded({ extended: false }));

        // on http://localhost:3000/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3000%2F&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Flovelace%3Fauth_callback%3D1&state=eyJoYXNzVXJsIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY2xpZW50SWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvIn0%3D
        this._app.get('/auth/authorize', (req, res) => {
            res.setHeader('location', `/frontend_es5/authorize.html${req.url.replace(/^\//, '').replace(/^auth\/authorize/, '')}`);
            res.status(302).send();
        });

        this._app.use('/auth/providers', (req, res) =>
            res.json([{id: null, name: 'ioBroker Local', type: 'iobroker'}]));

        this._app.post('/auth/login_flow', (req, res) => {
            console.log(`/auth/login_flow${JSON.stringify(req.query)}${JSON.stringify(req.body)}`);

            generateRandomToken((_err, token) => {
                this._auth_flows[token] = {ts: Date.now()};

                res.json({
                    data_schema: [
                        {name: 'username', type: 'string'},
                        {name: 'password', type: 'string'}
                    ],
                    description_placeholders: null,
                    errors: {},
                    flow_id: token,
                    handler: ['iobroker', null],
                    step_id: 'init',
                    type: 'form'
                });
            });
        });

        this._app.post('/auth/login_flow/:id', (req, res) => {
            if (!this._auth_flows[req.params.id] || Date.now() - this._auth_flows[req.params.id].ts > TIMEOUT_PASSWORD_ENTER) {
                return res.status(501).json({error: 'session expired'});
            }

            let s = '';
            req.on('data', d => s += d.toString());
            req.on('end', () => {
                try {
                    s = JSON.parse(s);
                } catch (_) {
                    this.log.warn(`Cannot parse with data: ${s}`);
                }

                console.log(`/auth/login_flow/:id${JSON.stringify(req.query)}${JSON.stringify(req.param)}`);

                this.adapter.checkPassword(s.username, s.password, result => {
                    if (result) {
                        generateRandomToken((_err, auth_code) => {
                            this._auth_flows[req.params.id].auth_code = auth_code;
                            this._auth_flows[req.params.id].client_id = s.client_id;
                            this._auth_flows[req.params.id].username  = s.username;
                            this._auth_flows[req.params.id].ts        = Date.now();

                            res.json({
                                description: null,
                                description_placeholders: null,
                                flow_id:    req.params.id,
                                handler:    ['iobroker', null],
                                result:     auth_code, // auth code
                                title:      'ioBroker Local',
                                type:       'create_entry',
                                version:    1
                            });
                        });
                    } else {
                        res.status(401).json({error: 'Invalid username or password'});
                    }
                });
            });
        });

        const cpUpload = upload.fields([
            {name: 'client_id',     maxCount: 1},
            {name: 'client_secret', maxCount: 1},
            {name: 'code',          maxCount: 1},
            {name: 'refresh_token', maxCount: 1},
            {code: 'grant_type',    maxCount: 1}
        ]);

        this._app.post('/auth/token', cpUpload, (req, res) =>
            this.onAuth(req, res));

        /*const token = this._app.oauth.token();
        this._app.post('/auth/token', (req,res,next) => {
            token(req,res,next);
        });*/

        const pagesWithAuth = [
            /^\/adapter\//,
            /^\/state\//,
            /^\/api\/history\//,
        ];

        this._app.use((req, res, next) => {
            if (req.url.includes('authorize.html')) {
                this.log.debug('authorize.html');
                res.send(this._renderAuthorize());
            } else {
                // check authentication - allow entity_icons to be delivered without token
                if (pagesWithAuth.some(reg => reg.test(req.url)) && this.config.auth !== false && !entityData.entityIconUrls.includes(req.url)) {
                    let access_token = req.headers ? (req.headers.authorization || req.headers.Authorization || '').substring(7) : ''; // "'Bearer '.length
                    access_token = access_token || req.query.token;
                    if (!access_token) {
                        this.log.warn('Auth token not found! Close connection.');
                        return res.status(401).send({error: 'Not authenticated'});
                    } else {
                        const now          = Date.now();
                        const flowId       = Object.keys(this._auth_flows)
                            .find(_flowId =>
                                this._auth_flows[_flowId].access_token === access_token &&
                                now - this._auth_flows[_flowId].ts < this._auth_flows[_flowId].auth_ttl);

                        if (!flowId) {
                            this.log.warn('Auth token is expired!');
                            res.status(401).json({error: 'Not authenticated'});
                            return;
                        } else {
                            req._user = this._auth_flows[flowId].username;
                        }
                    }
                }

                next();
            }
        });

        this._app.get('/cards/*', async (req, res) => await this.onCards(req, res));
        this._app.get('/lovelace/cards/*', async (req, res) => await this.onCards(req, res));

        this._app.get('/local/custom_ui/*', async (req, res) => await this.onCustomUI(req, res));
        this._app.get('/lovelace/local/custom_ui/*', async (req, res) => await this.onCustomUI(req, res));

        this._app.get('/', (req, res) => res.send(this._renderIndex()));
        this._app.get('/lovelace/', (req, res) => res.send(this._renderIndex()));
        this._app.get('/lovelace', (req, res) => res.send(this._renderIndex()));
        this._app.get('/lovelace/:id', (req, res) => res.send(this._renderIndex()));

        this._app.get('/manifest.json', (req, res) => res.json(this._renderManifest()));
        this._app.get('/lovelace/manifest.json', (req, res) => res.json(this._renderManifest()));

        this._app.use('/frontend_latest/', express.static(getRootPath()));
        this._app.use('/lovelace/frontend_latest/', express.static(getRootPath()));

        this._app.use('/static/icons/', express.static(__dirname + '/../assets/icons/'));
        this._app.use('/lovelace/static/icons/', express.static(__dirname + '/../assets/icons/'));

        this._app.use('/images/', express.static(getRootPath() + 'static/images/'));
        this._app.use('/lovelace/images/', express.static(getRootPath() + 'static/images/'));

        this._app.use('/static/', express.static(getRootPath()));
        this._app.use('/lovelace/static/', express.static(getRootPath()));

        this._app.use('/adapter/', async (req, res) => {
            let url = req.url;
            url = url.replace(/^\/([a-zA-Z0-9-_]+)\//, '/$1.admin/');
            url = url.split('/');
            // Skip first /
            url.shift();
            // Get ID
            const id = url.shift();
            url = url.join('/');
            const pos = url.indexOf('?');
            if (pos !== -1) {
                url = url.substring(0, pos);
            }

            try {
                const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
                const image = await this.adapter.readFileAsync(id, url, {user});
                if (image.file === null || image.file === undefined) {
                    throw new Error('File empty');
                } else {
                    image.mimeType = image.mimeType || (mime.getType || mime.lookup).call(image.mimeType, url) || 'text/javascript';
                    res.contentType(image.mimeType);
                    res.status(200).send(image.file);
                }
            } catch (err) {
                res.contentType('text/html');
                res.status(404).send('File ' + url + ' not found: ' + err);
            }
        });

        // Init read from states
        this._app.get('/state/*', async (req, res) => {
            try {
                const fileName = req.url.split('/', 3)[2].split('?', 2);
                const obj = await this.adapter.getForeignObjectAsync(fileName[0]);
                let contentType = 'text/plain';
                if (obj && obj.common.type === 'file')  {
                    contentType = (mime.getType || mime.lookup).call(mime, fileName[0]);
                }
                const user = await this._getUserId(this.config.defaultUser); //TODO: why is this always default user?
                const data = this.adapter.getBinaryStateAsync(fileName[0], {user});
                if (data !== null && obj !== undefined) {
                    if (data && typeof data === 'object' && data.val !== undefined && data.ack !== undefined) {
                        res.set('Content-Type', 'application/json');
                    } else {
                        res.set('Content-Type', contentType || 'text/plain');
                    }
                    res.status(200).send(data);
                } else {
                    res.status(404).send(`404 Not found. File ${fileName[0]} not found`);
                }
            } catch (e) {
                res.status(500).send('500. Error' + e);
            }
        });

        this._app.use('/lovelace/', express.static(getRootPath()));
        this._app.use(express.static(getRootPath()));

        this._app.get('/api/history/period/:start', async (req, res) => {
            this.log.debug(`Get history for ${req.query.filter_entity_id} from ${req.params.start} to ${req.query.end_time}`);

            const user = req._user || this.config.defaultUser;

            if (!this.adapter.config.history) {
                this.log.warn(`History instance is not selected in the settings`);
                res.json([]);
            } else {
                const entityIDs = req.query.filter_entity_id.split(',').map(id => id.trim());
                const totalResult = [];
                for (const id of entityIDs) {
                    //const i = entityIDs.indexOf(id);
                    // Get stored values for "system.adapter.admin.0.memRss" in last hour
                    const entity = entityData.entityId2Entity[id];
                    if (entity) {
                        const id = entity.context.STATE.getId || entity.context.STATE.setId;
                        try {
                            let stateResult;
                            const options = {
                                start: new Date(req.params.start).getTime(),
                                end: new Date(req.query.end_time).getTime(),
                                count: this.adapter.config.historyMaxCount,
                                aggregate: 'onchange',
                                user,
                            };
                            if (id) {
                                stateResult = await this.adapter.sendToAsync(this.adapter.config.history, 'getHistory', {
                                    id, options
                                });
                            } else {
                                stateResult = { result: [] };
                            }
                            const attributesResult = {};
                            if (entity.context.ATTRIBUTES) {
                                for (const attribute of entity.context.ATTRIBUTES) {
                                    const id = attribute.getId || attribute.setId;
                                    if (id) {
                                        attributesResult[attribute.attribute] = await this.adapter.sendToAsync(this.adapter.config.history,
                                            'getHistory', {id, options});
                                    } else {
                                        attributesResult[attribute.attribute] = {result: [] };
                                    }
                                }
                            }

                            //var t = [
                            //    [{
                            //      "attributes": {
                            //          "attribution": "Weather forecast from met.no, delivered by the Norwegian Meteorological Institute.",
                            //          "entity_picture": "https://api.met.no/weatherapi/weathericon/1.1/?symbol=None;content_type=image/png",
                            //          "friendly_name": "yr Symbol"
                            //      },
                            //      "context": {"id": "1969195c53b54e9593e3a7c50425fd9d", "parent_id": null, "user_id": null},
                            //      "entity_id": "sensor.yr_symbol",
                            //      "last_changed": "2019-05-21T11:14:39.714000+00:00",
                            //      "last_updated": "2019-05-21T11:14:39.714000+00:00",
                            //      "state": "unknown"
                            //      }]
                            //]

                            //match attributes to states by ts:
                            const getAttributeValues = (ts, attributesResult, attributesUsed = [], attributeValues = {}) => {
                                if (entity.context.ATTRIBUTES) {
                                    for (const attribute of entity.context.ATTRIBUTES) {
                                        if (attributesUsed.indexOf(attribute.attribute) >= 0) {
                                            continue; //already found value for that attribute.
                                        }
                                        let found = false;
                                        const results = attributesResult[attribute.attribute].result;
                                        for (const result of results) {
                                            if (result.val !== null && !result.used) {
                                                const resultTs = this._convertStateTStoISOString(result);
                                                if (Math.abs(new Date(resultTs).getTime() - new Date(ts).getTime()) < 1000) {
                                                    attributeValues[attribute.attribute] = typeof attribute.historyParser === 'function' ? attribute.historyParser(id, result.val) : result.val;
                                                    result.used = true;
                                                    found = true;
                                                    break;
                                                }
                                            }
                                        }
                                        if (!found) {
                                            attributeValues[attribute.attribute] = entity.attributes[attribute.attribute]; //use current value as default if none found.
                                        }
                                    }
                                }
                                for (const key of Object.keys(entity.attributes)) {
                                    if (!entity.context.ATTRIBUTES || !entity.context.ATTRIBUTES.find(a => a.attribute === key)) {
                                        attributeValues[key] = entity.attributes[key]; //make sure to copy static attributes here.
                                    }
                                }
                                return attributeValues;
                            };

                            //console.log(JSON.stringify(stateResult.result));
                            const arr = [];
                            for (const e of stateResult.result) {
                                if (e.val !== null) {
                                    const ts = this._convertStateTStoISOString(e);
                                    const result = {
                                        entity_id: entity.entity_id,
                                        state: typeof entity.context.STATE.historyParser === 'function' ?
                                            entity.context.STATE.historyParser(id, e.val).toString() :
                                            this._iobState2EntityState(id, e.val).toString(),
                                        last_changed: ts,
                                        last_updated: ts,
                                        attributes: getAttributeValues(ts, attributesResult)
                                    };
                                    arr.push(result);
                                }
                            }
                            //add unsused attribute values:
                            if (entity.context.ATTRIBUTES) {
                                const attributesUsed = [];
                                for (const attribute of entity.context.ATTRIBUTES) {
                                    attributesUsed.push(attribute.attribute);
                                    const results = attributesResult[attribute.attribute].result;
                                    for (const result of results) {
                                        if (result.val !== null && !result.used) {
                                            const attributeValues = {};
                                            const ts = this._convertStateTStoISOString(result);
                                            attributeValues[attribute.attribute] = typeof attribute.historyParser === 'function' ? attribute.historyParser(id, result.val) : result.val;
                                            result.used = true;
                                            //fill in other attributes:
                                            const data = {
                                                entity_id: entity.entity_id,
                                                //state: null,
                                                last_changed: ts,
                                                last_updated: ts,
                                                attributes: getAttributeValues(ts, attributesResult, attributesUsed, attributeValues)
                                            };
                                            arr.push(data);
                                        }
                                    }
                                }
                            }
                            //console.log(JSON.stringify(arr));

                            totalResult.push(arr);
                        } catch (e) {
                            this.log.warn(`Could not get history for ${entity.entity_id} - Error in request: ${e}`);
                            totalResult.push([]);
                        }
                    } else {
                        this.log.warn(`Cannot get history - Unknown entity: ${id}`);
                        totalResult.push([]); // place default value
                    }
                }
                res.json(totalResult);

                //res.status(500).send({error: 'Unknown entities'});
            }

        });
        this._app.get('/api/camera_proxy_stream/:entity_id', async (req, res) => {
            this.log.debug(`Get image for ${req.params.entity_id} token=${req.query.token || req.query.signed}`);
            try {
                const data = await this._getImage(req.params.entity_id, req.query.token || 'empty', req.query.signed);
                res.setHeader('content-type', data.content_type);
                res.send(Buffer.from(data.content, 'base64'));
            } catch (err) {
                this.log.warn(`Error in camera_proxy_stream: ${err} - ${err.stack}`);
                res.status(404).json({error: err});
            }
        });

        this._app.get('/api/camera_proxy/:entity_id', async (req, res) => {
            this.log.debug(`Get image for ${req.params.entity_id} acces_token=${req.query.signed}`);
            try {
                const data = await this._getImage(req.params.entity_id, 'empty', req.query.signed);
                res.setHeader('content-type', data.content_type);
                res.send(Buffer.from(data.content, 'base64'));
            } catch (err) {
                this.log.warn(`Error in camera_proxy: ${err} - ${err.stack}`);
                res.status(404).json({error: err});
            }
        });

        //initialize the WebSocket server instance
        this._wss = new WebSocket.Server({server: this._server});

        if (this.config.auth !== false) {
            this.adapter.getState('session', (err, state) => {
                if (state && state.val) {
                    try {
                        state = JSON.parse(state.val);
                    } catch (e) {
                        this.log.error(`Cannot parse session: ${state}`);
                        return;
                    }
                    this._auth_flows = state;
                }
            });
        }

        this._wss.on('connection', async ws => await this._initConnection(ws));
    }

    _getLayoutConfig() {
        return this._lovelaceConfig;
    }

    _setLayoutConfig(config) {
        this.adapter.getObject('configuration', (err, obj) => {
            if (JSON.stringify(obj.native) !== JSON.stringify(config)) {
                obj.native = config;
                this.adapter.setObject('configuration', obj, err => {
                    err && this.log.error(`Cannot save config: ${err}`);
                    this._sendUpdate('lovelace_updated');
                });
            }

        });
    }

    async _getUserId(user) {
        let userId = '';
        if (typeof this.adapter.getUserId === 'function') {
            try {
                const userObj = await this.adapter.getUserId(user);
                if (userObj) {
                    userId = userObj.id;
                }
            } catch (err) {
                this.log.warn(`Could not get user id for ${user} - ${err}`);
            }
        }
        if (!userId) {
            userId = 'system.user.' + user.toLowerCase(); //hack and not correct since js-controller 3.2
        }
        return userId;
    }

    async _getCurrentUser(ws) {
        const user = await this._getUserId(ws.__auth.username || this.config.defaultUser);
        const userObj = {
            id: user,
            name: 'Unknown',
            is_owner: user === 'system.user.admin',
            is_admin: user === 'system.user.admin',
            credentials: [{auth_provider_type: 'iobroker', auth_provider_id: null}],
            mfa_modules: [{id: 'totp', name: 'Authenticator app', enabled: false}]
        };

        try {
            const obj = await this.adapter.getForeignObjectAsync(user, {user});
            userObj.name = utils.getObjectName(obj);

            if (!userObj.is_admin) {
                //if user is not admin but can write config object -> make lovelace admin.
                const configObj = await this.adapter.getObjectAsync(this.adapter.namespace + '.configuration');
                if (configObj.acl) {
                    if (configObj.acl.owner === user) {
                        userObj.is_admin = true;
                        userObj.is_owner = true;
                    } else if (configObj.acl.ownerGroup) {
                        const groupObj = await this.adapter.getForeignObjectAsync(configObj.acl.ownerGroup);
                        if (groupObj.common.members && groupObj.common.members.includes(user)) {
                            userObj.is_admin = true;
                            userObj.is_owner = true;
                        }
                    }
                } else {
                    this.log.warn(`No ACL info in ${this.adapter.namespace + '.configuration'}. Can't determine if user is admin`);
                }
            }

        } catch (err) {
            this.log.warn('Could not get user ' + user + ': ' + (err.stack ? err.stack : err));
        }

        return userObj;
    }

    _getTranslations() {
        return {
            resources: require('./translations/de.json')
        };
    }

    _sendResponse(ws, id, result) {
        ws.send(JSON.stringify({id, type: 'result', success: true, result: result || null}));
    }

    _sendUpdate(eventType) {
        this._wss.clients.forEach(ws => {
            if (ws._subscribes && ws._subscribes[eventType]) {
                const t = {
                    type: 'event',
                    event: {
                        event_type: eventType,
                        origin: 'LOCAL',
                        time_fired: new Date().toISOString()
                    }
                };
                ws._subscribes[eventType].forEach(id => {
                    t.id = id;
                    ws.send(JSON.stringify(t));
                });
            }
        });
    }

    _processShoppingList(ws, message) {
        this.adapter.getState('control.shopping_list', (err, state) => {
            if (state && state.val) {
                try {
                    state = JSON.parse(state.val);
                } catch (e) {
                    this.log.error(`Cannot parse shopping list: ${state.val}`);
                    state = [];
                }
            } else {
                state = [];
            }
            if (message.type === 'shopping_list/items/add') {
                state.push({name: message.name, id: Date.now(), complete: false});
                this.adapter.setState('control.shopping_list', JSON.stringify(state), true, () => {
                    this._sendResponse(ws, message.id);
                    this._sendUpdate('shopping_list_updated');
                });
            } else if (message.type === 'shopping_list/items/clear') {
                state = state.filter(item => !item.complete);

                this.adapter.setState('control.shopping_list', JSON.stringify(state), true, () => {
                    this._sendResponse(ws, message.id);
                    this._sendUpdate('shopping_list_updated');
                });
            }  else if (message.type === 'shopping_list/items/update') {
                const item = state.find(item => item.id === message.item_id);
                if (item) {
                    if (message.name !== undefined) {
                        item.name = message.name;
                    }
                    if (message.complete !== undefined) {
                        item.complete = message.complete;
                    }
                    this.adapter.setState('control.shopping_list', JSON.stringify(state), true, () => {
                        this._sendResponse(ws, message.id);
                        this._sendUpdate('shopping_list_updated');
                    });
                } else {
                    this._sendResponse(ws, message.id, {result: false});
                }
            } else {
                this._sendResponse(ws, message.id, state);
            }
        });
    }

    async _getImage(entity_id, token, access_token) {
        const entity = entityData.entityId2Entity[entity_id];
        if (!entity) {
            throw new Error('no entity found');
        } else {
            let id;
            let userName = this.config.defaultUser;
            if (this.config.auth !== false && (token || access_token)) {
                if (access_token) {
                    const now = Date.now();
                    const flowId = Object.keys(this._auth_flows)
                        .find(flowId =>
                            this._auth_flows[flowId].access_token === access_token &&
                            now - this._auth_flows[flowId].ts < this._auth_flows[flowId].auth_ttl);

                    if (!flowId) {
                        throw new Error('Invalid token!');
                    } else {
                        userName = this._auth_flows[flowId].username;
                    }
                } else
                if (token && entity.attributes.access_token !== token) {
                    this.log.warn(`Invalid access token for ${entity_id} - ${token}`);
                    throw new Error(`Invalid access token for ${entity_id} - ${token}`);
                }
            }

            if (entity.context.STATE.getId) {
                id = entity.context.STATE.getId;
            } else if (entity.context.ATTRIBUTES) {
                const attr = entity.context.ATTRIBUTES.find(attr => attr.attribute === 'url');
                if (attr) {
                    id = attr.getId;
                }
            }
            if (id) {
                try {
                    const user = await this._getUserId(userName);
                    const state = await this.adapter.getForeignStateAsync(id, {user});
                    if (state && state.val && typeof state.val === 'string') {
                        const val = state.val.split('?')[0] || '';
                        let result;
                        // if like /adapter/daswetter/icons/tiempo-weather/galeria1/3.png
                        if (val.startsWith('/adapter/')) {
                            let url = val;
                            url = url.replace(/^\/adapter\/([a-zA-Z0-9-_]+)\//, '/$1.admin/');
                            url = url.split('/');
                            // Skip first /
                            url.shift();
                            // Get ID
                            const id = url.shift();
                            url = url.join('/');
                            const pos = url.indexOf('?');
                            if (pos !== -1) {
                                url = url.substring(0, pos);
                            }

                            const image = await this.adapter.readFileAsync(id, url, {user});
                            if (image) {
                                if (image.file === null || image.file === undefined) {
                                    throw new Error('Not found');
                                } else {
                                    image.mimeType = image.mimeType || (mime.getType || mime.lookup).call(image.mimeType, url) || 'text/javascript';
                                    result = {content_type: image.mimeType, content: image.file.toString('base64')};
                                }
                            }
                        } else if (state.val.startsWith('data')) {
                            const dataUrl = state.val;
                            const mimeType = dataUrl.substring(dataUrl.indexOf(':') + 1, dataUrl.indexOf(';'));
                            const encoding = dataUrl.substring(dataUrl.indexOf(';') + 1, dataUrl.indexOf(','));

                            if (encoding.localeCompare('base64', undefined, { sensitivity: 'base' }) !== 0) {
                                this.log.warn(`Wrong encoding: ${encoding}`);
                                throw new Error(`Wrong encoding: ${encoding}`);
                            }

                            const base64Data = dataUrl.split(',')[1];
                            result = {
                                content_type: mimeType || 'image/jpeg',
                                content: base64Data
                            };
                        } else {
                            const resp = await axios(state.val, {responseType: 'arraybuffer'});
                            result = {
                                content_type: (mime.getType || mime.lookup).call(mime, val) || 'image/jpeg',
                                content: Buffer.from(resp.data, 'binary').toString('base64')
                            };
                        }
                        return result;
                    } else {
                        throw new Error(`State ${id} does not contain url to image`);
                    }
                } catch (err) {
                    throw new Error(`Cannot download image: ${err}`);
                }
            } else {
                throw new Error(`No url attribute found for ${entity_id}`);
            }
        }
    }

    async _readNotifications() {
        try {
            const state = await this.adapter.getStateAsync('notifications.list');
            const val = (state && state.val) || '[]';
            try {
                this._notifications = JSON.parse(val);
            } catch (e) {
                this.log.warn(`Cannot parse notifications: ${val}`);
                this._notifications = [];
            }
            return this._notifications;
        } catch (e) {
            this.adapter.log.error(`Failed to read notifications: ${e}`);
        }
    }

    async _saveNotifications() {
        await this.adapter.setStateAsync('notifications.list', JSON.stringify(this._notifications), true);
        return this._notifications;
    }

    async addNotification(info) {
        if (typeof info !== 'object') {
            if (typeof info === 'string' && info.trim()[0] === '{') {
                try {
                    info = JSON.parse(info);
                } catch (e) {
                    console.warn(`Cannot parse ${info}`);
                }
            } else {
                info = {title: info.toString(), message: ''};
            }
        }
        info.notification_id = info.notification_id || Date.now();
        info.created_at = info.created_at || Date.now();

        this._notifications.push(info);
        return this._saveNotifications();
    }

    async _clearNotification(id) {
        if (id) {
            for (let i = 0; i < this._notifications.length; i++) {
                if (this._notifications[i].notification_id === id) {
                    this._notifications.splice(i, 1);
                    break;
                }
            }
        } else {
            this._notifications = [];
        }
        return this._saveNotifications();
    }

    async _initConnection(ws) {
        ws._subscribes = {};
        ws.__templates = [];
        let testTimer = null;

        ws.on('error', e => {
            console.error('Error: ' + e);
            testTimer && clearInterval(testTimer);
            testTimer = null;
        });

        //connection is up, let's add a simple simple event
        ws.on('message', async message => {
            try {
                message = JSON.parse(message);
            } catch (e) {
                console.error(`Cannot parse message: ${message}`);
                return;
            }

            // this.log.debug('MESSAGE: ' + message.type);
            const now = Date.now();

            if (message.type === 'auth') {
                this.log.debug(`HASS Auth: ${message.access_token}`);
                // simulate token exchange
                if (this.config.auth === false && message.access_token === NO_TOKEN) {
                    ws.__auth = {};
                    return ws.send(JSON.stringify({type: 'auth_ok', ha_version: VERSION}));
                }

                const flowId = Object.keys(this._auth_flows).find(flowId => this._auth_flows[flowId].access_token === message.access_token && now - this._auth_flows[flowId].ts < this._auth_flows[flowId].auth_ttl);
                if (!flowId) {
                    this.log.warn('HASS auth_invalid: ' + message.access_token);
                    ws.send(JSON.stringify({type: 'auth_invalid', result: false, ha_version: VERSION}));
                    ws.close();
                } else {
                    ws.__auth = this._auth_flows[flowId];
                    ws.__auth.flowId = flowId;
                    //ws.send(JSON.stringify({type: 'auth_required', ha_version: VERSION}));
                    ws.send(JSON.stringify({type: 'auth_ok', ha_version: VERSION}));
                }
                return;
            }

            // check authentication
            if (this.config.auth !== false) {
                if (!ws.__auth) {
                    this.log.warn('Auth token not found! Close connection.');
                    ws.send(JSON.stringify({type: 'auth_invalid', result: false, ha_version: VERSION}));
                    ws.close();
                    return;
                } else if (now - ws.__auth.ts > ws.__auth.auth_ttl) {
                    this.log.warn('Auth token is expired! Close connection.');
                    ws.send(JSON.stringify({type: 'auth_invalid', result: false, ha_version: VERSION}));
                    ws.close();
                    return;
                }
            }

            if (message.type === 'subscribe_events') {
                this.log.debug(`HASS Subscribe: ${message.event_type}`);
                if (ws._subscribes) {
                    ws._subscribes[message.event_type] = ws._subscribes[message.event_type] || [];
                    ws._subscribes[message.event_type].push(message.id);
                }
                this._sendResponse(ws, message.id);
            } else
            if (message.type === 'unsubscribe_events') {
                this.log.debug(`HASS UnSubscribe: ${JSON.stringify(message)}`);
                if (message.event_type && ws._subscribes && ws._subscribes[message.event_type]) {
                    const pos = ws._subscribes[message.event_type].indexOf(message.subscription);
                    if (pos !== -1) {
                        ws._subscribes[message.event_type].splice(pos, 1);
                    }

                    if (!ws._subscribes[message.event_type].length) {
                        delete ws._subscribes[message.event_type];
                    }
                } else if (!message.event_type) {
                    ws._subscribes && Object.keys(ws._subscribes).forEach(event_type => {
                        const pos = ws._subscribes[event_type].indexOf(message.subscription);
                        if (pos !== -1) {
                            ws._subscribes[event_type].splice(pos, 1);
                        }

                        if (!ws._subscribes[event_type].length) {
                            delete ws._subscribes[event_type];
                        }
                    });

                    if (ws.__templates) {
                        for (let i = ws.__templates.length - 1; i >= 0; i--) {
                            if (ws.__templates[i].id === message.subscription) {
                                ws.__templates.splice(i, 1);
                            }
                        }
                    }
                }

                this._sendResponse(ws, message.id);
            } else
            if (message.type === 'get_states') {
                this._sendResponse(ws, message.id, this.getHassStates());
            } else
            if (message.type === 'get_config') {
                this._sendResponse(ws, message.id, this._getConfig());
            } else
            if (message.type === 'get_services') {
                this._sendResponse(ws, message.id, SERVICES);
            } else
            if (message.type === 'get_panels') {
                this._sendResponse(ws, message.id, PANELS);
            } else
            if (message.type === 'auth/sign_path') {
                this.log.debug(`Sign: ${message.path}`);
                try {
                    const [path, query] = message.path.split('?');
                    const search = {};
                    if (query) {
                        query.split('&').forEach(item => {
                            const parts = item.split('=');
                            search[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1] || '');
                        });
                    }


                    search.signed = this.config.auth !== false ? ws.__auth.access_token : NO_TOKEN + '_' + Date.now();
                    const url = Object.keys(search).map(attr => `${encodeURIComponent(attr)}=${encodeURIComponent(search[attr])}`).join('&');
                    this._sendResponse(ws, message.id, {path: `${path}?${url}`});
                } catch (e) {
                    this.log.error(e);
                }
            } else
            if (message.type === 'frontend/get_themes') {
                this._sendResponse(ws, message.id, this._getThemes());
            } else
            if (message.type === 'auth/current_user') {
                this._getCurrentUser(ws).then(data =>
                    this._sendResponse(ws, message.id, data));
            } else
            if (message.type === 'frontend/get_user_data') {
                this.log.debug('Get USER Data: ' + message.key);
                this._sendResponse(ws, message.id, {value:{language: this.systemConfig.language}});
            } else
            if (message.type === 'frontend/get_translations') {
                this.log.debug('Get translations: ' + message.language);
                this._sendResponse(ws, message.id, this._getTranslations());
            } else
            if (message.type === 'lovelace/config') {
                this.log.debug('get config: ' + message.force);
                this._sendResponse(ws, message.id, this._getLayoutConfig());
            } else
            if (message.type === 'lovelace/config/save') {
                this._setLayoutConfig(message.config);
                this._sendResponse(ws, message.id);
            } else
            if (message.type === 'lovelace/resources') {
                this._sendResponse(ws, message.id, this._ressourceConfig);
            } else
            if (message.type === 'persistent_notification/get') {
                this._sendResponse(ws, message.id, this._notifications);
            } else
            if (message.type === 'config/entity_registry/get') {
                // todo What is that?
                this.log.debug('Get entity_registry: ' + message.entity_id);
                this._sendResponse(ws, message.id, null);
            } else
            if (message.type.startsWith('shopping_list/items')) {
                this._processShoppingList(ws, message);
            } else
            if (message.type === 'camera_thumbnail') {
                this.log.warn('camera_thumbnail ' + message.entity_id + ' deprecated!!!');
                try {
                    const data = await this._getImage(message.entity_id, null, null);
                    this._sendResponse(ws, message.id, data);
                } catch (err) {
                    this._sendResponse(ws, message.id);
                }
            } else
            if (message.type === 'call_service') {
                //{"type":"call_service","domain":"zone","service":"turn_off","service_data":{"entity_id":"zone.home"},"id":18}
                await this._processCall(ws, message);
            }  else
            if (message.type === 'conversation/agent/info') {
                // BF: I have no idea what is that
                this._sendResponse(ws, message.id, {onboarding: null, attribution: null});
            } else
            if (message.type === 'conversation/process') {
                ws.__conversations = ws.__conversations || {};
                // request is like {"type":"conversation/process","text":"aaa","conversation_id":"edd087a3779a6b2a5e4d","id":23}
                const conversation_id = message.conversation_id;

                ws.__conversations[conversation_id] = {
                    id: message.id,
                    timer: setTimeout((_conversation_id, messageId) => {
                        ws && ws.__conversations && ws.__conversations[_conversation_id] && delete ws.__conversations[_conversation_id];
                        ws && this._sendResponse(ws, messageId, {
                            speech: {
                                plain: {
                                    speech: this.words.no_one_hears_you[this.lang] || this.words.no_one_hears_you.en,
                                    extra_data: null
                                }
                            }
                        });
                    }, 1000, conversation_id, message.id),
                };

                this.adapter.setState('conversation', message.text);

                // answer must be
                // {"id": 23, "type": "result", "success": true, "result": {"speech": {"plain": {"speech": "Sorry, I didn't understand that", "extra_data": null}}, "card": {}}}
            } else
            if (message.type === 'render_template') {
                //{"type":"render_template","template":"The **Markdown** ","variables":{"config":{"type":"markdown","content":"The **Markdown** card allows you to write any text. You can style it **bold**, *italicized*, ~strikethrough~ etc. You can do images, links, and more.\n\nFor more information see the [Markdown Cheatsheet](https://commonmark.org/help).","title":"Markdown"}},"id":11}
                const template = message.template;
                this._sendResponse(ws, message.id);

                const obj = {template, ids: [], id: message.id};
                ws.__templates && ws.__templates.push(obj);

                const vars = bindings.extractBinding(template);
                const promises = [];

                // subscribe all variables and read their values
                vars && vars.forEach(v =>
                    promises.push(new Promise(resolve => {
                        const id = v.systemOid;

                        obj.ids.push(id);

                        if (this._subscribed.indexOf(id) === -1) {
                            this._subscribed.push(id);
                            this.adapter.subscribeForeignStates(id);
                            this.log.debug('IoB Subscribe on ' + id);
                        }

                        this.adapter.getForeignState(id, (err, state) => {
                            if (state) {
                                this.templateStates[id] = state;
                            }
                            resolve();
                        });
                    })));

                Promise.all(promises)
                    .then(() => {
                        const t = {
                            id: message.id,
                            type: 'event',
                            event: {
                                result: bindings.formatBinding(template, this.templateStates)
                            }
                        };
                        ws.send(JSON.stringify(t));
                    });
            } else {
                console.warn(`Unknown request: ${JSON.stringify(message)}`);
            }
        });

        /*testTimer = setInterval(() => {
            // {id: 2, type: "event", "event": {"event_type": "state_changed", "data": {"entity_id": "sun.sun", "old_state": {"entity_id": "sun.sun", "state": "above_horizon", "attributes": {"next_dawn": "2019-05-17T02:57:08+00:00", "next_dusk": "2019-05-16T19:44:32+00:00", "next_midnight": "2019-05-16T23:21:40+00:00", "next_noon": "2019-05-17T11:21:38+00:00", "next_rising": "2019-05-17T03:36:58+00:00", "next_setting": "2019-05-16T19:04:54+00:00", "elevation": 54.81, "azimuth": 216.35, "friendly_name": "Sun"}, "last_changed": "2019-05-16T09:09:53.424242+00:00", "last_updated": "2019-05-16T12:46:30.001390+00:00", "context": {id: "05356b1a7df54b2f939d3c7f8a3e05b4", "parent_id": null, "user_id": null}}, "new_state": {"entity_id": "sun.sun", "state": "above_horizon", "attributes": {"next_dawn": "2019-05-17T02:57:08+00:00", "next_dusk": "2019-05-16T19:44:32+00:00", "next_midnight": "2019-05-16T23:21:40+00:00", "next_noon": "2019-05-17T11:21:38+00:00", "next_rising": "2019-05-17T03:36:58+00:00", "next_setting": "2019-05-16T19:04:54+00:00", "elevation": 54.71, "azimuth": 216.72, "friendly_name": "Sun"}, "last_changed": "2019-05-16T09:09:53.424242+00:00", "last_updated": "2019-05-16T12:47:30.000414+00:00", "context": {id: "e738dc26af1d48b4964c6d9805179595", "parent_id": null, "user_id": null}}}, "origin": "LOCAL", "time_fired": "2019-05-16T12:47:30.000414+00:00", "context": {id: "e738dc26af1d48b4964c6d9805179595", "parent_id": null, "user_id": null}}}
            if (subscribes.state_changed) {
                subscribes.state_changed.forEach(id => {
                    const t = {
                        id,
                        type: "event",
                        "event": {
                            "event_type": "state_changed",
                            "data": {
                                "entity_id": "sun.sun",
                                "old_state": {
                                    "entity_id": "sun.sun",
                                    "state": "above_horizon",
                                    "attributes": {
                                        "next_dawn": "2019-05-17T02:57:08+00:00",
                                        "next_dusk": "2019-05-16T19:44:32+00:00",
                                        "next_midnight": "2019-05-16T23:21:40+00:00",
                                        "next_noon": "2019-05-17T11:21:38+00:00",
                                        "next_rising": "2019-05-17T03:36:58+00:00",
                                        "next_setting": "2019-05-16T19:04:54+00:00",
                                        "elevation": 54.81,
                                        "azimuth": 216.35,
                                        "friendly_name": "Sun"
                                    },
                                    "last_changed": "2019-05-16T09:09:53.424242+00:00",
                                    "last_updated": "2019-05-16T12:46:30.001390+00:00",
                                    "context": {
                                        id: "05356b1a7df54b2f939d3c7f8a3e05b4",
                                        "parent_id": null,
                                        "user_id": null
                                    }
                                },
                                "new_state": {
                                    "entity_id": "sun.sun",
                                    "state": "above_horizon",
                                    "attributes": {
                                        "next_dawn": "2019-05-17T02:57:08+00:00",
                                        "next_dusk": "2019-05-16T19:44:32+00:00",
                                        "next_midnight": "2019-05-16T23:21:40+00:00",
                                        "next_noon": "2019-05-17T11:21:38+00:00",
                                        "next_rising": "2019-05-17T03:36:58+00:00",
                                        "next_setting": "2019-05-16T19:04:54+00:00",
                                        "elevation": 54.71,
                                        "azimuth": 216.72,
                                        "friendly_name": "Sun"
                                    },
                                    "last_changed": "2019-05-16T09:09:53.424242+00:00",
                                    "last_updated": "2019-05-16T12:47:30.000414+00:00",
                                    "context": {
                                        id: "e738dc26af1d48b4964c6d9805179595",
                                        "parent_id": null,
                                        "user_id": null
                                    }
                                }
                            },
                            "origin": "LOCAL",
                            "time_fired": "2019-05-16T12:47:30.000414+00:00",
                            "context": {id: "e738dc26af1d48b4964c6d9805179595", "parent_id": null, "user_id": null}
                        }
                    };
                    ws.send(JSON.stringify(t));
                });
            }
        }, 10000);*/
        ws.on('close', () => {
            this.log.debug('Connection closed');
            ws._subscribes = null;
            ws.__templates = null;
            testTimer && clearInterval(testTimer);
            testTimer = null;
        });
    }

    _saveAuth(cb) {
        if (this.config.auth !== false) {
            this.adapter.setState('session', JSON.stringify(this._auth_flows), cb);
        }
    }

    /**
     * Update entity (by deleting and recreating it)
     * @param {string} id of entity.context.id -> main id / device id.
     * @param {ioBroker.Object} obj of entity.context.id
     * @param entity old entity.
     * @param {string} [forcedEntityId] force entity to have a different entity_id (used in conflict cases)
     * @returns {Promise<void>}
     */
    async _updateEntity(id, obj, entity, forcedEntityId) {
        if (entity) {
            this.log.debug(`Processing change in ${id}, removing old entity ${entity.entity_id} in order to recreate.`);
            utils.removeEntity(entity);
        } else {
            this.log.debug(`No entity in change set for ${id}. Might create new entities.`);
        }

        //object deleted:
        if (!obj) {
            //we have an entity that is missing it's object -> need to delete.
            if (entity) {
                this.log.debug(`Object ${id} deleted. ${entity.entity_id} was deleted, too.`);
            }
            return [];
        } else {
            //update manual device:
            if (obj.common && obj.common.custom && obj.common.custom[this.adapter.namespace]) {
                this.log.debug(`Object ${id} changed, required update of manual entity.`);
                return this._processManualEntity(id, forcedEntityId);
            }

            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.`);
                } else {
                    return this._createOneDevice(id, forcedEntityId); //start auto creation for one device.
                }
            } else {
                return this._createOneDevice(id, forcedEntityId); //start auto creation for one device.
            }
        }
        return [];
    }

    /**
     * Marks id and obj or entity for later update. Updates entries, if there are already some for entity / id.
     * @param {String} id of iobroker object
     * @param {ioBroker.Object} obj
     * @param {Entity} [entity]
     * @private
     */
    _markForUpdate(id, obj, entity) {
        if (entity) { //we have entity -> keep only on change for one entity, because it will be completely rebuild.
            const foundIndex = this._objectData.updatedObjects.findIndex(e => e.entity && e.entity.entity_id === entity.entity_id);
            if (foundIndex !== -1) {
                this.log.debug(`Overwriting to process change later on! New: ${obj ? 'object' : 'NO object'}, ${id}. Old: ${!!this._objectData.updatedObjects[foundIndex].obj ? 'object' : 'NO object'}, ${id} for ${entity.entity_id}`);
                this._objectData.updatedObjects[foundIndex] = {id, obj, entity};
            } else {
                this.log.debug(`Storing to process change later on! New: ${obj ? 'object' : 'NO object'}, ${id} for ${entity.entity_id}`);
                this._objectData.updatedObjects.push({id, obj, entity});
            }
        } else {
            const foundIndex = this._objectData.updatedObjects.findIndex(e => !e.entity && e.id === id);
            if (foundIndex !== -1) {
                this.log.debug(`Overwriting to process change later on! New: ${obj ? 'object' : 'NO object'}, ${id}. Old: ${!!this._objectData.updatedObjects[foundIndex].obj ? 'object' : 'NO object'}, ${id} for no entity yet`);
                this._objectData.updatedObjects[foundIndex] = {id, obj};
            } else {
                this.log.debug(`Storing to process change later on! New: ${obj ? 'object' : 'NO object'}, ${id} for no entity, yet`);
                this._objectData.updatedObjects.push({id, obj});
            }
        }
    }

    async onObjectChange(id, obj) {
        if (obj) {
            if (obj.type === 'state' || obj.type === 'channel' || obj.type === 'device') {
                if (!this.adapter.config.aliasOnly || id.startsWith('alias.0.') || (obj.common && obj.common.custom && obj.common.custom[this.adapter.namespace])) {
                    this._objectData.objects[id] = obj;
                    this.log.debug(`${id} updated.`);
                    if (!this._objectData.ids.includes(id)) {
                        this._objectData.ids.push(id);
                    }
                }
            } else if (obj.type === 'enum') {
                if (id.startsWith('enum.rooms.')) {
                    const {ids, entities: affectedEntities} = utils.findEntitiesFromEnumChange(obj, this._objectData.rooms[id]);
                    for (const entity of affectedEntities) {
                        if (entity) {
                            this.log.debug(`${id} changed, ${entity.entity_id} affected.`);
                            this._markForUpdate(entity.context.id, this._objectData.objects[entity.context.id], entity);
                        }
                    }
                    for (const id of ids) {
                        this._markForUpdate(id, this._objectData.objects[id]);
                    }
                    this._objectData.rooms[id] = obj;
                    if (obj) {
                        this._objectData.roomNames[id] = utils.getEnumName(obj, this.lang);
                        this.log.debug(`enum ${id} updated.`);
                    }
                }
                if (id.startsWith('enum.functions.')) {
                    const {ids, entities: affectedEntities} = utils.findEntitiesFromEnumChange(obj, this._objectData.functions[id]);
                    for (const entity of affectedEntities) {
                        if (entity) {
                            this.log.debug(`${id} changed, ${entity.entity_id} affected.`);
                            this._markForUpdate(entity.context.id, this._objectData.objects[entity.context.id], entity);
                        }
                    }
                    for (const id of ids) {
                        this._markForUpdate(id, this._objectData.objects[id]);
                    }
                    this._objectData.functions[id] = obj;
                    if (obj) {
                        this._objectData.funcNames[id] = utils.getEnumName(obj, this.lang);
                        this.log.debug(`enum ${id} updated.`);
                    }
                }
            }
        } else {
            delete this._objectData.objects[id];
            delete this._objectData.rooms[id];
            delete this._objectData.functions[id];
            const foundIndex = this._objectData.ids.indexOf(id);
            if (foundIndex !== -1) {
                this._objectData.ids.splice(foundIndex, 1);
            }
            this.log.debug(`${id} deleted.`);
        }

        if (id === this.adapter.namespace + '.configuration') {
            this._lovelaceConfig = obj.native;
            //let user change hideHeader during runtime:
            this._lovelaceConfig.hideToolbar = this._lovelaceConfig.hideToolbar === undefined ? this.config.hideHeader : this._lovelaceConfig.hideToolbar;
            this._manageSubscribesFromConfig();
            await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
        } else if (id === 'system.config') {
            if (obj && obj.common) {
                this.lang = obj.common.language || this.lang || 'en';
                entityData.lang = this.lang;
                this.systemConfig = obj.common;
                this._updateConstantEntities();
                this.log.debug(`${id} -> config updated, constant entities updated.`);
                await this.adapter.setStateAsync('info.entitiesUpdated', true, true);
            }
        } else {
            //remove all entities that are connected to this id and check if we should recreate them (later):
            let affectedEntities = entityData.iobID2entity[id] || [];
            if (obj) {
                this._objectData.objects[id] = obj; //if had custom and now no custom anymore -> store new info about object here!
            }
            if (obj && obj.type !== 'device') { //if not device, search in parent for device.
                const parts = id.split('.');
                while(parts.length > 3) {
                    parts.pop();
                    const parentId = parts.join('.');
                    const entities = entityData.iobID2entity[parentId];
                    if (entities) {
                        affectedEntities = affectedEntities.concat(entities);
                    }
                    if (this._objectData.objects[id] && this._objectData.objects[id].type === 'device') {
                        break;
                    }
                }
            }
            if (affectedEntities && affectedEntities.length) {
                for (const entity of affectedEntities) {
                    this.log.debug(`${id} changed, ${entity.entity_id} affected.`);
                    this._markForUpdate(entity.context.id, this._objectData.objects[entity.context.id], entity);
                }
            } else if (obj) {
                this.log.debug(`${id} changed, no entity affected, will check if new entity can be created..`);
                this._markForUpdate(id, obj); //no old entities affected.
            }

            this._updateTimer && clearTimeout(this._updateTimer);
            this._updateTimer = setTimeout(async () => {
                this._updateTimer = null;
                const promises = [];
                let needUpdate = false;
                this.log.debug(`Update timer expired, ${this._objectData.updatedObjects.length} objects updated.`);
                const idsWithUpdate = [];

                //recreate all entities:
                while (this._objectData.updatedObjects.length > 0) {
                    const {id, obj, entity} = this._objectData.updatedObjects.shift();
                    this.log.debug(`Processing ${entity ? entity.entity_id : 'undefined'} for ${id}`);
                    needUpdate = needUpdate || !!entity;
                    promises.push((async () => {
                        const entities = await this._updateEntity(id, obj, entity);
                        for (const e of entities) {
                            this.log.debug(`Object ${id} changed, update of ${e.entity_id} required.`);
                            needUpdate = true;
                            utils.fillEntityIntoCaches(e);
                            if (!idsWithUpdate.includes(id)) {
                                idsWithUpdate.push(id);
                            }
                        }
                    })());
                }
                await Promise.all(promises); //fine because we do not really need them to be in order.
                this.log.debug(`All ${promises.length} changes done.`);

                if (needUpdate) {
                    await this._getAllStates();
                    this._manageSubscribesFromConfig();
                    await this.adapter.setStateAsync('info.entitiesUpdated', true, true);

                    for (const id of idsWithUpdate) {
                        await this.onStateChange(id, null, true);
                    }
                    this.log.debug('Had changes, updated states and notified entitiesUpdated state.');
                }
            }, 5000);
        }
    }

    destroy(cb) {
        this.adapter.unsubscribeStates('control.*');
        this.adapter.unsubscribeStates('notifications.*');
        this._saveAuth(() => {
            this.adapter.unsubscribeForeignObjects('*');
            this.adapter.unsubscribeObjects('configuration', cb);
        });
        this._clearInterval && clearInterval(this._clearInterval);
        this._clearInterval = null;
        this._updateTimer && clearTimeout(this._updateTimer);
        this._updateTimer = null;
    }
}

module.exports = WebServer;