UNPKG

iobroker.tahoma

Version:
1,364 lines (1,194 loc) 71 kB
const request = require('request'); let controller; function Tahoma(username, password, url, localUrl, loginOptions, applyOptions, context) { controller = this; controller.context = context; controller.username = username; controller.password = password; controller.url = url; controller.local_url = localUrl; controller.use_local_api = controller.local_url !== ''; if (controller.use_local_api) { controller.context.log.info('Adapter will connect to Tahoma Box via local API.'); } controller.loginOptions = loginOptions; controller.applyOptions = applyOptions; controller.lastEventTime = new Date().getTime(); controller.rawDeviceData = false; controller.tahomaJar = request.jar(); controller.req_defaults = { headers: { // 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0', }, rejectUnauthorized: !controller.use_local_api, jar: controller.tahomaJar, }; controller.baseRequest = request.defaults(controller.req_defaults); controller.localRequest = null; controller.baerer_token = null; controller.tahomaDevices = {}; controller.tahomaActionGroups = {}; controller.Map_DeviceURL2StateName = {}; controller.isConnectedInternal = false; controller.loginInProgress = false; controller.gatewayList = []; controller.gatewayTimer = null; controller.eventRegisterID = '-1'; controller.loginAttempts = 0; controller.loginErrorTimeout = null; controller.applyQueue = []; controller.applyTimer = null; controller.applyProcessing = false; controller.retryTimeout = null; controller.loginTimeout = null; controller.reapplyTimeout = null; controller.lastRelogin = null; controller.failedRequests = 0; controller.retryLocked = false; controller.lastRefresh = 0; controller.deviceExecIds = {}; controller.ackStateValues = {}; controller.existingSlowStates = {}; controller.events = {}; } Tahoma.prototype.unload = function (callback) { if (controller.loginErrorTimeout) { clearTimeout(controller.loginErrorTimeout); } if (controller.applyTimer) { clearTimeout(controller.applyTimer); } if (controller.retryTimeout) { clearTimeout(controller.retryTimeout); } if (controller.loginTimeout) { clearTimeout(controller.loginTimeout); } if (controller.reapplyTimeout) { clearTimeout(controller.reapplyTimeout); } if (controller.gatewayTimer) { clearTimeout(controller.gatewayTimer); } callback(); }; Tahoma.prototype.isConnected = function () { return controller.isConnectedInternal; }; Tahoma.prototype.setConnected = function (connected) { controller.isConnectedInternal = connected; controller.context.setState('info.connection', true, true); }; Tahoma.prototype.getCreateStateOptions4Widget = function (widget) { switch (widget) { case 'PositionableHorizontalAwning': return { role: 'blind', features: { deployment: true, percentage: true, closure: false, orientation: false, slow: false, }, }; case 'UpDownHorizontalAwning': return { role: 'blind', features: { deployment: true, percentage: false, closure: false, orientation: false, slow: false, }, }; case 'UpDownCurtain': case 'UpDownDualCurtain': case 'UpDownExteriorScreen': case 'UpDownExteriorVenetianBlind': case 'UpDownRollerShutter': case 'UpDownScreen': case 'UpDownVenetianBlind': case 'UpDownSwingingShutter': return { role: 'blind', features: { deployment: false, percentage: false, closure: true, orientation: false, slow: false, }, }; case 'BioclimaticPergola': return { role: 'blind', features: { deployment: false, percentage: false, closure: false, orientation: true, slow: false, }, }; case 'PositionableRollerShutterWithLowSpeedManagement': return { role: 'blind', features: { deployment: false, percentage: true, closure: true, orientation: false, slow: true, }, }; case 'PositionableScreen': case 'PositionableScreenUno': case 'PositionableHorizontalAwningUno': case 'PositionableRollerShutter': case 'PositionableTiltedRollerShutter': case 'PositionableRollerShutterUno': case 'PositionableTiltedScreen': case 'PositionableTiltedWindow': case 'PositionableGarageDoor': case 'DiscretePositionableGarageDoor': case 'AwningValance': return { role: 'blind', features: { deployment: false, percentage: true, closure: true, orientation: false, slow: false, }, }; case 'LuminanceSensor': return { role: 'sensor', }; case 'DimmerLight': return { role: 'light.dimmer', features: { intensity: true, }, }; default: return { role: 'state', }; } }; Tahoma.prototype.getCreateStateOptions4State = function (widget, stateName) { if ( stateName === 'core:ClosureState' || stateName === 'core:TargetClosureState' || stateName === 'core:DeploymentState' || stateName === 'core:TargetDeploymentState' ) { return { type: 'number', // optional, default "number" read: true, // mandatory, default true write: true, // mandatory, default true min: 0, // optional, default 0 max: 100, // optional, default 100 unit: '%', // optional, default % role: 'level.blind', // mandatory }; } else if (stateName === 'core:TargetTemperatureState') { return { type: 'number', // optional, default "number" read: true, // mandatory, default true write: true, // mandatory, default true min: 5, // optional, min according to Somfy specs max: 26, // optional, max according to Somfy specs role: 'thermo', // mandatory }; } else if (stateName === 'core:SlateOrientationState') { return { type: 'number', // optional, default "number" read: true, // mandatory, default true write: true, // mandatory, default true min: 0, // optional, default 0 max: 100, // optional, default 100 unit: '%', // optional, default % role: 'level.blind.orientation', // mandatory }; } else if (stateName === 'core:LuminanceState') { return { type: 'number', // optional, default "number" read: true, // mandatory, default true write: false, // mandatory, default true min: 0, // optional, default 0 max: 100000, // optional, default 100 unit: 'Lux', // optional, default % role: 'level.color.luminance', // mandatory }; } else if (stateName === 'core:OnOffState') { return { type: 'string', // optional, default "number" read: true, // mandatory, default true write: true, // mandatory, default true role: 'switch', // mandatory }; } else if (stateName === 'core:LightIntensityState') { return { type: 'number', // optional, default "number" read: true, // mandatory, default true write: true, // mandatory, default true min: 0, // optional, default 0 max: 100, // optional, default 100 unit: 'W', // optional, default W role: 'level.dimmer', // mandatory }; } return { read: true, write: false, role: 'state', }; }; Tahoma.prototype.sendPOST = function (requestPath, payload, callback, locally) { controller.login(function (err, data) { if (err) { return callback(err, data); } controller.sendInternalPOST(requestPath, payload, callback, locally); }); }; Tahoma.prototype.sendGET = function (requestPath, payload, callback, locally) { controller.login(function (err, data) { if (err) { return callback(err, data); } controller.sendInternalGET(requestPath, payload, callback, locally); }); }; Tahoma.prototype.sendDELETE = function (requestPath, payload, callback, locally) { controller.login(function (err, data) { if (err) { return callback(err, data); } controller.sendInternalDELETE(requestPath, payload, callback, locally); }); }; Tahoma.prototype.sendInternalRequest = function (method, requestPath, payload, callback, locally) { let url; if (locally) { url = controller.local_url + requestPath; } else { url = controller.url + requestPath; } if (!locally && requestPath.endsWith('apply') && payload && payload.actions && payload.actions.length === 1) { // only on POST // do not abuse high Priority! It will result in err 400 on too many requests url = `${controller.url + requestPath}/highPriority`; } let formPayload = null; let jsonPayload = null; const requestParams = { url, }; if (requestPath === 'login') { // only on POST formPayload = payload; } else { jsonPayload = payload; } if (requestPath === 'login') { controller.context.log.debug(`${method} request on ${url} with payload: +++redacted+++`); } else { controller.context.log.debug(`${method} request on ${url} with payload:${JSON.stringify(payload)}`); } let reqFunc; let sendFunc; let baseReq; if (locally) { baseReq = controller.localRequest; } else { baseReq = controller.baseRequest; } if (method === 'POST') { reqFunc = baseReq.post; sendFunc = controller.sendPOST; } else if (method === 'GET') { reqFunc = baseReq.get; sendFunc = controller.sendGET; } else if (method === 'DELETE') { reqFunc = baseReq.delete; sendFunc = controller.sendDELETE; } else { controller.context.log.warn(`Invalid method for request: ${method}`); callback && callback(true, {}); return; } if (!locally || method === 'POST') { if (jsonPayload !== null) { requestParams.json = jsonPayload; } if (formPayload !== null) { requestParams.form = formPayload; } } reqFunc( requestParams, function (error, response, body) { if (!error && response && (response.statusCode === 200 || response.statusCode === 204)) { controller.failedRequests = 0; if (requestPath === 'login') { // only on POST callback(false, JSON.parse(body)); } else { controller.context.log.debug(`Response: ${JSON.stringify(body)}`); callback(false, body); } } else if ( response && requestPath !== 'logout' && (response.statusCode === 401 || response.statusCode === 403) ) { controller.context.log.warn( `error during request: ${response.statusMessage} ->${response.statusCode} retry ${requestPath}`, ); // session expired? if (locally) { controller.clearBearerToken(); } controller.setConnected(false); controller.loginInProgress = false; if (controller.loginErrorTimeout) { clearTimeout(controller.loginErrorTimeout); } if (controller.loginAttempts >= controller.loginOptions.maxAttempts) { controller.loginAttempts = 0; // sleep for two minutes controller.context.log.info( `Login failed ${controller.loginOptions.maxAttempts} times, waiting ${ controller.loginOptions.delayAfterFailure } seconds before retrying.`, ); if (controller.loginTimeout) { clearTimeout(controller.loginTimeout); } if (controller.retryTimeout) { controller.context.log.info('Timeout still active.'); } else { controller.retryTimeout = setTimeout(function () { controller.context.log.info('Executing Timeout (retry login).'); controller.retryTimeout = null; sendFunc(requestPath, payload, callback); }, controller.loginOptions.delayAfterFailure * 1000); } } else { controller.loginAttempts++; controller.context.log.debug( `Will attempt again to login in ${controller.loginOptions.delayAttempts} seconds.`, ); controller.loginErrorTimeout = setTimeout(function () { controller.context.log.info(`Login attempt #${controller.loginAttempts}`); // perform login and send again sendFunc(requestPath, payload, callback); }, controller.loginOptions.delayAttempts * 1000); } } else if (response && (response.statusCode === 400 || response.statusCore === 503)) { controller.failedRequests++; if ( !controller.use_local_api || (requestPath !== 'events/register' && !requestPath.match(/events\/*.\/fetch/g)) ) { if (requestPath === 'login') { controller.context.log.warn( `error during request: ${error}, request path: ${requestPath} with payload: +++redacted+++`, ); } else { controller.context.log.warn( `error during request: ${error}, request path: ${requestPath} with payload:${JSON.stringify( payload, )}`, ); } if (response) { controller.context.log.warn(`Response: ${JSON.stringify(response)}`); } if (body) { controller.context.log.warn(`Body: ${JSON.stringify(body)}`); } } if (controller.use_local_api) { callback(true, response); return; } // Only reregister event listener if (requestPath.match(/events\/*.\/fetch/g) && body?.error === 'No registered event listener.') { controller.context.log.warn( 'Apparently the event-listener expired server side. Creating a new one...', ); controller.retryTimeout = setTimeout(function () { controller.retryTimeout = null; controller.sendPOST( 'events/register', {}, function (err, data) { if (err && controller.use_local_api) { controller.context.log.debug( 'events/register failed. falling back to setup/devices', ); controller.getSetup(callback); return; } controller.eventRegisterID = data.id; controller.context.log.info(`New eventRegisterID = ${controller.eventRegisterID}`); // controller.fetchEvents(callback); controller.sendInternalRequest(method, requestPath, payload, callback, locally); }, controller.use_local_api, ); }, 10000); } else if ( controller.failedRequests > 5 || (body && body.errorCode && body.errorCode === 'UNSPECIFIED_ERROR') ) { let delay; if (response.statusCode === 503) { // down for maintenance delay = 30000; } else { // problem with request delay = 10000; } if (controller.retryTimeout) { clearTimeout(controller.retryTimeout); } controller.retryLocked = true; controller.context.log.warn(`Waiting ${delay / 1000} until re-login because of too many errors.`); controller.retryTimeout = setTimeout(function () { controller.retryTimeout = null; controller.logout(function (err) { controller.context.log.warn(`Failure during logout: ${err}`); controller.retryLocked = false; callback(true, response); }); }, delay); } else { callback(true, response); } } else { if (requestPath === 'login') { controller.context.log.warn( `error during request: ${error}, request path: ${requestPath} with payload: +++redacted+++`, ); } else { controller.context.log.warn( `error during request: ${error}, request path: ${requestPath} with payload:${JSON.stringify( payload, )}`, ); } if (response) { controller.context.log.warn(`Response: ${JSON.stringify(response)}`); } if (body) { controller.context.log.warn(`Body: ${JSON.stringify(body)}`); } const result = {}; result.error = error; if (typeof response !== 'undefined') { controller.context.log.debug(`response status: ${response.statusCode} ${response.statusMessage}`); result.responseStatusCode = response.statusCode; result.responsestatusMessage = response.statusMessage; } callback(true, result); } }, controller.use_local_api, ); }; Tahoma.prototype.clearBearerToken = function () { controller.context.log.info(`Clearing stored bearer token ${controller.context.config.bearer_token}`); controller.context.getForeignObject(`system.adapter.${controller.context.namespace}`, (err, obj) => { if (!err && obj) { obj.native.bearer_token = ''; controller.context.setForeignObject(`system.adapter.${controller.context.namespace}`, obj); } }); }; Tahoma.prototype.sendInternalGET = function (requestPath, payload, callback, locally) { return controller.sendInternalRequest('GET', requestPath, payload, callback, locally); }; Tahoma.prototype.sendInternalPOST = function (requestPath, payload, callback, locally) { return controller.sendInternalRequest('POST', requestPath, payload, callback, locally); }; Tahoma.prototype.sendInternalDELETE = function (requestPath, payload, callback, locally) { return controller.sendInternalRequest('DELETE', requestPath, payload, callback, locally); }; Tahoma.prototype.logout = function (callback) { if (controller.use_local_api) { // do not log out on enabled local api callback(false, {}); return true; } const performLogout = controller.isConnected(); controller.setConnected(false); if (controller.applyTimer) { controller.applyProcessing = true; // avoid executing mistakenly clearTimeout(controller.applyTimer); controller.applyTimer = null; } controller.eventRegisterID = '-1'; if (performLogout) { controller.sendInternalPOST('logout', {}, function (err, data) { callback(err, data); }); } else { callback(false, {}); } return true; }; Tahoma.prototype.login = function (callback) { if (controller.isConnected()) { callback(false, {}); return; } if (controller.local_url.length > 0 && controller.context.config.bearer_token) { controller.lastEventTime = new Date().getTime(); controller.setConnected(true); controller.loginInProgress = false; const newDefaults = controller.req_defaults; newDefaults.headers['Authorization'] = `Bearer ${controller.context.config.bearer_token}`; newDefaults.headers['Content-type'] = 'application/json'; controller.localRequest = request.defaults(newDefaults); controller.context.log.info(`Using stored bearer token ${controller.context.config.bearer_token}`); controller.gatewayTimer = setTimeout(function () { controller.updateGateWays(); }, 10000); return controller.getSetup(callback); } // check for login already started but not yet finished if (controller.loginInProgress) { controller.loginTimeout = setTimeout(function () { controller.loginTimeout = null; controller.login(callback); }, 1500); return; } controller.loginInProgress = true; const payload = { userId: controller.username, userPassword: controller.password, }; controller.sendInternalPOST('login', payload, function (err, data) { if (err || !data.success) { controller.loginInProgress = false; return callback(true, data); } controller.lastEventTime = new Date().getTime(); controller.setConnected(true); controller.loginInProgress = false; if (controller.use_local_api) { // we need a bearer token for the local api // first get token controller.sendInternalGET( `config/${controller.context.config.gatewaypin}/local/tokens/generate`, {}, function (err, data) { if (typeof data !== 'object') { data = JSON.parse(data); } if (!err && data.token) { controller.context.log.info(`Got token for local api: ${data.token}`); const bearerToken = data.token; // activate token const tokendata = { label: 'Toto token', token: bearerToken, scope: 'devmode', }; controller.sendInternalPOST( `config/${controller.context.config.gatewaypin}/local/tokens`, tokendata, function (err, data) { if (!err) { controller.context.config.bearer_token = bearerToken; const newDefaults = controller.req_defaults; newDefaults.headers['Authorization'] = `Bearer ${bearerToken}`; newDefaults.headers['Content-type'] = 'application/json'; controller.localRequest = request.defaults(newDefaults); controller.context.getForeignObject( `system.adapter.${controller.context.namespace}`, (err, obj) => { if (err) { controller.context.log.error( `Error while getting bearer token: ${err}`, ); callback(err, data); } if (obj) { obj.native.bearer_token = bearerToken; controller.context.setForeignObject( `system.adapter.${controller.context.namespace}`, obj, ); } }, ); controller.gatewayTimer = setTimeout(function () { controller.updateGateWays(); }, 10000); return controller.getSetup(callback); } controller.setConnected(false); controller.loginInProgress = false; callback(err, data); }, ); } else { controller.setConnected(false); controller.loginInProgress = false; controller.context.log.warn('Error getting token for local api'); callback(err, data); } }, ); } else { // using remote api controller.getUserInfo(function (err, data) { if (!err) { controller.gatewayTimer = setTimeout(function () { controller.updateGateWays(); }, 10000); return controller.getSetup(callback); } callback(err, data); }); } }); }; Tahoma.prototype.getUserInfo = function (callback) { controller.sendGET('enduser/mainAccount', {}, function (err, data) { if (!err) { controller.updateData('userdata', data.endUser); callback(false, data); } else { controller.context.log.warn('enduser/mainAccount failed!'); } }); }; Tahoma.prototype.updateGateWayData = function (gateways) { controller.gatewayList = []; for (const i in gateways) { const gateway = gateways[i]; if (gateway.gatewayId) { controller.gatewayList.push(gateway.gatewayId); controller.updateData(gateway.gatewayId, gateway); } } }; Tahoma.prototype.updateDevices = function (devices) { controller.tahomaDevices = devices; controller.context.log.debug(JSON.stringify(devices)); for (const i in devices) { const device = devices[i]; // just set the raw data from tahoma device.label = device.label.replace(/\./g, '_'); device.label = device.label.replace(/__/g, '_'); device.label = device.label.replace(controller.context.FORBIDDEN_CHARS, '_'); controller.updateDevice(`devices.${device.label}`, device); } }; Tahoma.prototype.updateDevice = function (name, deviceData) { const features = controller.getCreateStateOptions4Widget(deviceData.definition?.widgetName); const objset = { role: features.role, read: true, write: false, }; controller.createOrUpdateState(name, '', objset); // device URL controller.createOrUpdateState(`${name}.deviceURL`, deviceData.deviceURL); // states for (const stateKey in deviceData.states) { const state = deviceData.states[stateKey]; if (typeof state.value === 'undefined') { controller.context.log.debug( `State ${name}.states.${state.name} is undefined. Tahoma has no use for it, hence ignoring it.`, ); } else if (typeof state.value === 'object') { // Prepare sub-states const channelStates = []; for (const [key, value] of Object.entries(state.value)) { channelStates.push({ name: key, value }); } // Create a channel with state-sublelements controller.createOrUpdateChannel(`${name}.states.${state.name}`, channelStates); } else { controller.createOrUpdateState( `${name}.states.${state.name}`, controller.mapValueTahoma2ioBroker(state.name, state.value), controller.getCreateStateOptions4State(deviceData.widget, state.name), ); if (features.features && features.features.slow && features.features.slow === true) { const validStates = ['core:ClosureState', 'core:TargetClosureState']; if (validStates.includes(state.name)) { controller.existingSlowStates[`${name}.states.${state.name}`] = true; controller.createOrUpdateState( `${name}.states.${state.name}:slow`, controller.mapValueTahoma2ioBroker(state.name, state.value), controller.getCreateStateOptions4State(deviceData.widget, state.name), ); } } } } // commands for (const commandKey in deviceData.definition.commands) { const command = deviceData.definition.commands[commandKey]; // nparams for commands indicates how may parameters a command expects if ( (deviceData.deviceURL.startsWith('io:') && command.nparams === 0) || (deviceData.deviceURL.startsWith('rts:') && command.nparams <= 1) || (deviceData.deviceURL.startsWith('ogp:') && command.nparams <= 1) || (deviceData.deviceURL.startsWith('enocean:') && command.nparams === 0) ) { controller.createOrUpdateState(`${name}.commands.${command.commandName}`, false, { type: 'boolean', read: true, write: true, role: 'button', }); if (features.features && features.features.slow && features.features.slow === true) { const validCommands = ['up', 'open', 'down', 'close']; if (validCommands.includes(command.commandName)) { controller.existingSlowStates[`${name}.commands.${command.commandName}`] = true; controller.createOrUpdateState(`${name}.commands.${command.commandName}:slow`, false, { type: 'boolean', read: true, write: true, role: 'button', }); } } } } // raw data if (controller.rawDeviceData) { for (const p in deviceData) { const value = deviceData[p]; if (typeof value === 'object') { controller.updateData(`raw.${name}.${p}`, value); } else { controller.createOrUpdateState(`raw.${name}.${p}`, value); } } } }; Tahoma.prototype.updateActionGroups = function (actionGroups) { controller.tahomaActionGroups = actionGroups; for (const i in actionGroups) { const actionGroup = actionGroups[i]; // just set the raw data from tahoma controller.updateActionGroup(`actionGroups.${actionGroup.label}`, actionGroup); } }; Tahoma.prototype.updateActionGroup = function (actionGroup, actionGroupData) { // Action Group OID controller.createOrUpdateState(`${actionGroup}.oid`, actionGroupData.oid); controller.createOrUpdateState(`${actionGroup}.commands.` + `execute`, false, { type: 'boolean', read: true, write: true, role: 'button', }); }; Tahoma.prototype.mapValueTahoma2ioBroker = function (stateName, stateValue) { switch (stateName) { case 'core:ClosureState': case 'core:TargetClosureState': case 'core:DeploymentState': case 'core:TargetDeploymentState': case 'core:SlateOrientationState': case 'core:LuminanceState': case 'core:PriorityLockTimerState': case 'core:RSSILevelState': case 'core:LightIntensityState': case 'core:TemperatureState': case 'internal:LightingLedPodModeState': case 'internal:AlarmDelayState': case 'core:BatteryLevelState': case 'core:TargetTemperatureState': case 'core:TargetRoomTemperatureState': case 'core:DerogationEndDateTimeState': case 'core:DerogationStartDateTimeState': case 'io:ManualModeTargetTemperatureState': case 'core:DerogatedTargetTemperatureState': case 'core:ColorTemperatureState': stateValue = parseInt(stateValue, 10); break; case 'core:MovingState': stateValue = stateValue || stateValue === 'true'; break; } return stateValue; }; Tahoma.prototype.mapValueioBroker2Tahoma = function (stateName, stateValue) { if ( stateName === 'core:ClosureState' || stateName === 'core:TargetClosureState' || stateName === 'core:DeploymentState' || stateName === 'core:TargetDeploymentState' ) { // stateValue = parseInt(stateValue,10); } return stateValue; }; Tahoma.prototype.updateData = function (type, data) { for (const p in data) { const value = data[p]; if (typeof value === 'object') { controller.updateData(`${type}.${p}`, value); } else { controller.createOrUpdateState(`${type}.${p}`, value); } } }; Tahoma.prototype.createOrUpdateState = function (key, value, options) { // controller.context.log.debug('createOrUpdateState: ' + key + ' => ' + value); const stateName = key.substr(key.lastIndexOf('.') + 1); key = key.replace(/ /g, '_'); key = key.replace(controller.context.FORBIDDEN_CHARS, '_'); let typeName = 'string'; if (value === 'true' || value === 'false' || value === true || value === false) { value = value === 'true' || value === true; typeName = 'boolean'; } else if (Number.isInteger(value)) { value = parseInt(value, 10); typeName = 'number'; } else if (!isNaN(value)) { value = Number(value); typeName = 'number'; } controller.context.getObject(key, function (err, obj) { if (err || !obj) { if (typeof options === 'undefined') { options = { read: true, write: false, type: typeName, }; } if (!options.name) { options.name = stateName; } if (!options.role) { options.role = 'state'; } if (!options.type) { options.type = typeName; } // controller.context.log.debug('createState ' + key + ' => ' + value); // create state controller.context.setObject( key, { type: 'state', common: options, native: {}, }, function () { controller.ackStateValues[key] = value; controller.context.setState(key, value, true); }, ); } else { controller.context.log.debug(`setState ${key} => ${value}`); controller.ackStateValues[key] = value; controller.context.setState(key, value, true); } }); }; Tahoma.prototype.createOrUpdateChannel = function (key, states) { key = key.replace(/ /g, '_'); key = key.replace(controller.context.FORBIDDEN_CHARS, '_'); controller.context.setObjectNotExists( key, { type: 'channel', common: { read: true, write: false, }, }, () => { states.forEach(state => { if (typeof state.value === 'object') { // If there is a way more complex structure at this point, the adapter will ignore for now. state.value = 'Data too complex.'; } controller.context.log.debug(`Creating sub-state: ${key}.${state.name}`); controller.createOrUpdateState( `${key}.${state.name}`, controller.mapValueTahoma2ioBroker(state.name, state.value), controller.getCreateStateOptions4State(null, state.name), ); }); }, ); }; Tahoma.prototype.updateGateWays = function (nextEntry) { controller.gatewayTimer = null; if (!nextEntry) { nextEntry = 0; } else if (nextEntry >= controller.gatewayList.length) { controller.gatewayTimer = setTimeout(function () { controller.updateGateWays(); }, 30000); return; } controller.context.log.debug(`Update gateway status for id ${controller.gatewayList[nextEntry]}`); let useUrl = 'setup/gateways'; if (!controller.use_local_api) { useUrl = `${useUrl}/${controller.gatewayList[nextEntry]}`; } controller.sendGET( useUrl, {}, function (err, data) { if (!err && data) { controller.updateGateWayData([data]); controller.context.log.debug(`Updated gateway status for id ${controller.gatewayList[nextEntry]}`); } controller.updateGateWays(nextEntry + 1); }, controller.use_local_api, ); }; Tahoma.prototype.getSetup = function (callback) { controller.sendGET( 'setup', {}, function (err, data) { if (!err) { if (typeof data !== 'object') { data = JSON.parse(data); } controller.updateGateWayData(data.gateways); controller.updateData('location', data.location); controller.updateDevices(data.devices); // delete old devices controller.deleteOldDevices(); let toProcess = 0; // update mapping table device URL to state key with label controller.context.getAdapterObjects(function (res) { const objKeys = Object.keys(res); const search = new RegExp(`^${controller.context.namespace}\\.devices\\..+\\.deviceURL$`); for (let i = 0; i < objKeys.length; i++) { const objid = objKeys[i]; if (objid.match(search)) { toProcess++; controller.context.getState(objid, function (err, state) { if (!err && state) { controller.Map_DeviceURL2StateName[state.val] = objid.substr( 0, objid.indexOf('.deviceURL'), ); } toProcess--; if (toProcess < 1) { // now we should be able to refresh (devices known) controller.refresh(callback); } }); } else if (i === objKeys.length - 1 && toProcess < 1) { // now we should be able to refresh (devices known) controller.refresh(callback); } } }); } else { controller.context.log.warn('setup failed!'); controller.context.log.warn(JSON.stringify(data)); callback(err, {}); } }, controller.use_local_api, ); if (!controller.use_local_api) { controller.sendGET('actionGroups', {}, function (err, data) { if (!err) { controller.updateActionGroups(data); } else { controller.context.log.warn('actionGroups failed!'); controller.context.log.warn(JSON.stringify(data)); } // callback(err, {}); }); } }; Tahoma.prototype.refresh = function (callback) { // controller.one is overridden below?! if (controller.use_local_api) { callback && callback(false, {}); } else { controller.sendPOST('setup/devices/states/refresh', null, function (err, data) { if (err) { controller.context.log.warn( `refresh device state failed: Error - ${err} data: ${JSON.stringify(data)}`, ); } callback && callback(err, {}); }); } }; Tahoma.prototype.getAllStates = function (callback) { if (!controller.use_local_api && controller.retryLocked) { return; } controller.login(function (err, _data) { if (err) { return; } if (controller.eventRegisterID === '-1') { // currently the event registration does not work on local api (UNKOWN OBJECT) – waiting for firmware update! // as workaround we do "setup" again … controller.sendPOST( 'events/register', {}, function (err, data) { if (err && controller.use_local_api) { controller.context.log.debug('events/register failed. falling back to setup/devices'); controller.getSetup(callback); return; } controller.eventRegisterID = data.id; controller.context.log.info(`eventRegisterID = ${controller.eventRegisterID}`); controller.fetchEvents(callback); }, controller.use_local_api, ); } else { controller.fetchEvents(callback); } }); }; Tahoma.prototype.fetchEvents = function (callback) { const curTime = new Date().getTime(); if (controller.lastRefresh < curTime - 5 * 60 * 1000) { controller.context.log.debug('Refreshing device states.'); controller.lastRefresh = curTime; controller.refresh(function () { controller.fetchEvents(callback); }); return; } controller.sendPOST( `events/${controller.eventRegisterID}/fetch`, {}, function (err, data) { if (err) { return; } controller.context.log.debug( `events/${controller.eventRegisterID}/fetch` + `Fetched Data ${JSON.stringify(data)}`, ); controller.updateDeviceStateFromEvent(data); if (callback) { callback(); } }, controller.use_local_api, ); }; Tahoma.prototype.updateDeviceStateFromEvent = function (events) { for (const i in events) { controller.lastEventTime = new Date().getTime(); const event = events[i]; if (event.name === 'DeviceStateChangedEvent') { controller.updateDeviceState(event); } else if (event.name === 'ExecutionStateChangedEvent' || event.name === 'ExecutionRegisteredEvent') { controller.updateEventState(event); } } }; Tahoma.prototype.updateEventState = function (event) { controller.context.log.debug(`Event: ${JSON.stringify(event)}`); const execId = event.execId; const currentState = event.state || event.newState; try { if (event.name === 'ExecutionRegisteredEvent') { controller.events[event.execId] = { deviceURL: event.actions[0].deviceURL, command: event.actions[0].command || event.actions[0].commands[0].name, parameters: event.actions[0].parameters, state: event.actions[0].state, }; } if (currentState !== 'IN_PROGRESS' && currentState !== 'COMPLETED' && currentState !== 'FAILED') { return; } let url = 'exec/current/'; if (currentState === 'COMPLETED' || currentState === 'FAILED') { const storedEvent = controller.events[event.execId]; controller.context.log.debug( `Skipping calling history. Using stored event (${JSON.stringify(storedEvent)}) instead`, ); // Workaround: If the event cant be found in stored events, just ignore it for now // This will probably cause some command states not to be updated in some cases, but is better than log-spaming warn-messages. if (storedEvent === undefined) { controller.context.log.debug( `Could not find event for execId ${event.execId} locally. Ignoring it for now.`, ); return; } storedEvent.state = currentState; controller.updateDeviceActionState(storedEvent, execId); delete controller.events[event.execId]; return; } url += execId; if (!controller.use_local_api) { url += `?_=${new Date().getTime()}`; } controller.sendGET( url, {}, function (err, data) { if (err) { controller.context.log.warn(`Failed getting execution state for ${execId}`); return; } else if (!data || (!data.execution && !data.state)) { return; } controller.context.log.debug(`${url} - Fetched Data ${JSON.stringify(data)}`); let actions; if (data.execution) { // unify contents data = data.execution; actions = data.commands; } else { if (data.actionGroup) { actions = data.actionGroup.actions; } else if (data.commands) { actions = data.commands; } } let action; for (let i = 0; i < actions.length; i++) { action = actions[i]; if (action.commands) { const cmd = action.commands[0]; action.type = cmd.type; action.name = cmd.name; action.parameters = cmd.parameters; } if (currentState === 'IN_PROGRESS' || currentState === 'FAILED') { // Acknowledge that the command state was set, so the user can see something is happening const devicePath = controller.Map_DeviceURL2StateName[action.deviceURL]; let command = action.command ? action.command : action.name; if (command === 'setClosureAndLinearSpeed') { command = action.parameters[0] === 100 ? 'down:slow' : 'up:slow'; } const commandStateId = `${devicePath}.commands.${command}`; controller.context.getState(commandStateId, (err, tmpState) => { if (err || tmpState === nu