UNPKG

iobroker.nuki

Version:
1,547 lines (1,360 loc) 56.1 kB
/** * * nuki adapter * * /* jshint -W097 */// jshint strict:false /*jslint node: true */ 'use strict'; // you have to require the utils module and call adapter function var utils = require('@iobroker/adapter-core'); // Get common adapter utils var express = require('express'); // call express var bodyParser = require("body-parser"); var request = require('request'); var crypto = require('crypto'); var nacl = require('tweetnacl'); // cryptographic functions var buffer = require("buffer"); // nacl.utils = require('tweetnacl-util'); // encoding & decoding // var nsecret = require('tweetnacl.secret'); // const _sodium = require('libsodium-wrappers'); // REST server var app = express(); var timer = null; var ipInfo = require('ip'); var hostIp = ipInfo.address(); // Global variables var bridgeId = null; var bridgeType = 0; var bridgeHwId = null; var bridgeFwVer = null; var bridgeWifiFwVer = null; var bridgeAppVer = null; var bridgeIp = null; var bridgePort = '0'; var bridgeToken = null; var forcePlainToken = null; var bridgeName = null; var interval = null; var hostCb = null; var cbSet = false; var callbackId = null; var hostPort = null; var timeOut = 3000; var actionTimeOut, sleepTimeOut; var semver = require('semver'); // you have to call the adapter function and pass a options object // name has to be set and has to be equal to adapters folder name and main file name excluding extension // adapter will be restarted automatically every time as the configuration changed, e.g system.adapter.template.0 //var adapter = new utils.Adapter('nuki'); let adapter; function startAdapter(options) { options = options || {}; Object.assign(options, {name: 'nuki'}); adapter = new utils.Adapter(options); //adapter.log.debug('Adapter generated'); // adapter.useFormatDate = true; // load from ;system.config the global date format // Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ... adapter.on('message', function (obj) { if (typeof obj === 'object' && obj.message) { if (obj.command === 'send') { // e.g. send email or pushover or whatever console.log('send command'); // Send response in callback if required if (obj.callback) adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback); } } }); // is called when databases are connected and adapter received configuration. // start here! adapter.on('ready', function () { main(); }); // is called when adapter shuts down - callback has to be called under any circumstances! adapter.on('unload', function (callback) { try { if (actionTimeOut) clearTimeout(actionTimeOut); if (sleepTimeOut) clearTimeout(sleepTimeOut); if (timer) clearInterval(timer); if (cbSet) { hostCb = false; removeCallback(callbackId); } adapter.log.info('cleaned everything up...'); callback(); } catch (e) { callback(); } }); // is called if a subscribed object changes adapter.on('objectChange', function (id, obj) { // Warning, obj can be null if it was deleted adapter.log.debug(`objectChange ${id} ${JSON.stringify(obj)}`); }); // is called if a subscribed state changes adapter.on('stateChange', function (id, state) { let path = id.split('.',5); let nukiId = path[2]; let actionState = path[4]; // Warning, state can be null if it was deleted adapter.log.debug(`stateChange ${id} ${JSON.stringify(state)}`); // you can use the ack flag to detect if it is status (true) or command (false) if (state && !state.ack) { if (actionState == 'action') { setLockAction(nukiId, state.val); } else { if (state.val == false) { if (actionState == 'lockAction') { setLockAction(nukiId, '2'); } else if (actionState == 'rtoAction') { setLockAction(nukiId, '2'); } } else { switch (actionState) { case 'lockAction': // fall through case 'rtoAction': setLockAction(nukiId, '1'); break; case 'openAction': setLockAction(nukiId, '3'); break; case 'unlockLocknGoAction': // fall through case 'cmActiveAction': setLockAction(nukiId, '4'); break; case 'openLocknGoAction': // fall through case 'cmDeactiveAction': setLockAction(nukiId, '5'); break; default: adapter.log.warn(`unrecognized actionState (${actionState})`); break; } } } } }); return adapter; }; function initBridgeStates(_name, _token) { let timeStamp = new Date().toISOString().substr(0,19) + '+00:00'; adapter.setObjectNotExists(`${bridgeId}`, { type: 'device', common: { name: _name }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info`, { type: 'channel', common: { name: 'Info' }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.bridgeIp`, { type: 'state', common: { name: 'IP-Adresse', type: 'string', write: false, role: 'info.ip', def: bridgeIp }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.bridgePort`, { type: 'state', common: { name: 'Port', type: 'string', write: false, role: 'info.port', def: bridgePort }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.bridgeToken`, { type: 'state', common: { name: 'Token', type: 'string', write: false, role: 'text', def: _token }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.bridgeType`, { type: 'state', common: { name: 'Typ', type: 'number', write: false, states: { 1: 'Hardware Bridge', 2: 'Software Bridge', }, role: 'value', def: bridgeType }, native: {} }); if (bridgeType == 1) { adapter.setObjectNotExists(`${bridgeId}.info.hardwareId`, { type: 'state', common: { name: 'HardwareID', type: 'string', write: false, role: 'text', def: bridgeHwId }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.firmwareVersion`, { type: 'state', common: { name: 'Firmware', type: 'string', write: false, role: 'text' }, native: {} }); adapter.setObjectNotExists(`${bridgeId}.info.wifiFirmwareVersion`, { type: 'state', common: { name: 'WiFi Firmware', type: 'string', write: false, role: 'text' }, native: {} }); } else { adapter.setObjectNotExists(`${bridgeId}.info.appVersion`, { type: 'state', common: { name: 'App Version', type: 'string', write: false, role: 'text' }, native: {} }); } adapter.setObjectNotExists(`${bridgeId}.info.timestamp`, { type: 'state', common: { name: 'Zuletzt aktualisiert', type: 'string', write: false, role: 'date' }, native: {} }); setBridgeState(timeStamp); } function initNukiDeviceStates(_obj) { let nukiState = _obj.lastKnownState; let deviceType = 1; let firmwareVersion = ''; // device adapter.setObjectNotExists(`${_obj.nukiId}`, { type: 'device', common: { name: _obj.name }, native: {} }); // device info adapter.setObjectNotExists(`${_obj.nukiId}.info`, { type: 'channel', common: { name: 'Information' }, native: {} }); // device states adapter.setObjectNotExists(`${_obj.nukiId}.states`, { type: 'channel', common: { name: 'Status' }, native: {} }); if (_obj.hasOwnProperty('deviceType')) { deviceType = _obj.deviceType; } else { deviceType = get_devicetype_by_statename(nukiState.stateName) } switch(deviceType) { case 0: initNukiLockStates(_obj.nukiId); break; case 2: initNukiOpenerStates(_obj.nukiId); break; case 3: initNukiLockStates(_obj.nukiId); break; case 4: initNukiLockStates(_obj.nukiId); break; default: adapter.log.error(`Unknown device type (${deviceType}). Setting minimal states.`); deviceType = 1; break; } adapter.setObjectNotExists(`${_obj.nukiId}.info.deviceType`, { type: 'state', common: { name: 'Typ', type: 'number', write: false, states: { 0: 'Nuki Smart Lock 1.0/2.0', 1: 'unknown device', 2: 'Nuki Opener', 3: 'Nuki Smart Door', 4: 'Nuki Smart Lock 3.0 (Pro)', }, def: deviceType, role: 'value' }, native: {} }); if (_obj.hasOwnProperty('firmwareVersion')) { firmwareVersion = _obj.firmwareVersion adapter.setObjectNotExists(`${_obj.nukiId}.info.firmwareVersion`, { type: 'state', common: { name: 'Firmware', type: 'string', write: false, role: 'text', def: firmwareVersion }, native: {} }); } if (nukiState.hasOwnProperty('ringactionState')) { adapter.setObjectNotExists(`${_obj.nukiId}.states.ringactionState`, { type: 'state', common: { name: 'Klingel betätigt', type: 'boolean', write: false, role: 'indicator' }, native: {} }); // listen to changes adapter.subscribeStates(`${_obj.nukiId}.states.ringactionState`); } if (nukiState.hasOwnProperty('ringactionTimestamp')) { adapter.setObjectNotExists(`${_obj.nukiId}.states.ringactionTimestamp`, { type: 'state', common: { name: 'Letzte Klingelbetätigung', type: 'string', write: false, role: 'date' }, native: {} }); } adapter.setObjectNotExists(`${_obj.nukiId}.info.batteryCritical`, { type: 'state', common: { name: 'Batterie schwach', type: 'boolean', write: false, role: 'indicator.lowbat' }, native: {} }); // listen to changes adapter.subscribeStates(`${_obj.nukiId}.info.batteryCritical`); if (nukiState.hasOwnProperty('batteryCharging')) { adapter.setObjectNotExists(`${_obj.nukiId}.info.batteryCharging`, { type: 'state', common: { name: 'Batterie lädt', type: 'boolean', write: false, role: 'indicator.maintenance.lowbat' }, native: {} }); // listen to changes adapter.subscribeStates(`${_obj.nukiId}.info.batteryCharging`); } if (nukiState.hasOwnProperty('batteryChargeState')) { adapter.setObjectNotExists(`${_obj.nukiId}.info.batteryChargeState`, { type: 'state', common: { name: 'Ladezustand der Batterie', type: 'number', write: false, role: 'value.battery' }, native: {} }); // listen to changes adapter.subscribeStates(`${_obj.nukiId}.info.batteryChargeState`); } if (nukiState.hasOwnProperty('keypadBatteryCritical')) { adapter.setObjectNotExists(`${_obj.nukiId}.info.keypadBatteryCritical`, { type: 'state', common: { name: 'KeyPad-Batterie schwach', type: 'boolean', write: false, role: 'indicator.lowbat' }, native: {} }); // listen to changes adapter.subscribeStates(`${_obj.nukiId}.info.keypadBatteryCritical`); } adapter.setObjectNotExists(`${_obj.nukiId}.states.timestamp`, { type: 'state', common: { name: 'Zuletzt aktualisiert', type: 'string', write: false, role: 'date' }, native: {} }); // set states setLockState(_obj.nukiId, deviceType, nukiState, firmwareVersion); } function initNukiLockStates(_nukiId) { let doorsensorState = 4; adapter.setObjectNotExists(`${_nukiId}.info.mode`, { type: 'state', common: { name: 'Modus', type: 'number', write: false, states: { 2: 'door mode', 3: '-', }, role: 'value' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.states.lockState`, { type: 'state', common: { name: 'Nuki aufgeschlossen', type: 'boolean', write: false, role: 'sensor.lock' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.states.state`, { type: 'state', common: { name: 'Status', type: 'number', write: false, states: { 0: 'uncalibrated', 1: 'locked', 2: 'unlocking', 3: 'unlocked', 4: 'locking', 5: 'unlatched', 6: 'unlocked (lock n go)', 7: 'unlatching', 253: '-', 254: 'motor blocked', 255: 'undefined', }, role: 'value' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.states.doorState`, { type: 'state', common: { name: 'Türsensor', type: 'number', write: false, states: { 1: 'deactivated', 2: 'door closed', 3: 'door opened', 4: 'door state unknown', 5: 'calibrating', }, role: 'value', def: doorsensorState }, native: {} }); // device actions adapter.setObjectNotExists(`${_nukiId}.actions`, { type: 'channel', common: { name: 'Aktionen' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.action`, { type: 'state', common: { name: 'Aktion', type: 'number', states: { 0: '', 1: 'unlock', 2: 'lock', 3: 'unlatch', 4: 'lock‘n’go', 5: 'lock‘n’go with unlatch', }, role: 'value' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.lockAction`, { type: 'state', common: { name: 'Tür auf-/abschließen', type: 'boolean', write: true, role: 'switch.lock.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.openAction`, { type: 'state', common: { name: 'Tür öffnen', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.unlockLocknGoAction`, { type: 'state', common: { name: 'Tür aufschließen (lock‘n’go)', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.openLocknGoAction`, { type: 'state', common: { name: 'Tür öffnen (lock‘n’go)', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); // listen to changes adapter.subscribeStates(`${_nukiId}.actions.*Action`); adapter.subscribeStates(`${_nukiId}.actions.action`); } function initNukiOpenerStates(_nukiId) { adapter.setObjectNotExists(`${_nukiId}.info.mode`, { type: 'state', common: { name: 'Modus', type: 'number', write: false, states: { 2: 'door mode', 3: 'continuous mode', }, role: 'value' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.states.lockState`, { type: 'state', common: { name: 'Ring to Open aktiv', type: 'boolean', write: false, role: 'indicator' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.states.state`, { type: 'state', common: { name: 'Status', type: 'number', write: false, states: { 0: 'untrained', 1: 'online', 2: '-', 3: 'rto active', 4: '-', 5: 'open', 6: '-', 7: 'opening', 253: 'boot run', 254: '-', 255: 'undefined', }, role: 'value' }, native: {} }); // device actions adapter.setObjectNotExists(`${_nukiId}.actions`, { type: 'channel', common: { name: 'Aktionen' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.action`, { type: 'state', common: { name: 'Aktion', type: 'number', states: { 0: '', 1: 'activate rto', 2: 'deactivate rto', 3: 'electric strike actuation', 4: 'activate continuous mode', 5: 'deactivate continuous mode', }, role: 'value' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.rtoAction`, { type: 'state', common: { name: 'Ring to Open de-/aktivieren', type: 'boolean', write: true, role: 'switch.lock.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.openAction`, { type: 'state', common: { name: 'öffnen', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.cmActiveAction`, { type: 'state', common: { name: 'Dauermodus einschalten', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); adapter.setObjectNotExists(`${_nukiId}.actions.cmDeactiveAction`, { type: 'state', common: { name: 'Dauermodus ausschalten', type: 'boolean', write: true, read: false, role: 'button.open.door' }, native: {} }); // listen to changes adapter.subscribeStates(`${_nukiId}.actions.*Action`); adapter.subscribeStates(`${_nukiId}.actions.action`); } function setBridgeState(_timestamp) { if (bridgeType == 1) { // set firmware version adapter.setState(`${bridgeId}.info.firmwareVersion`, {val: bridgeFwVer, ack: true}); // set WiFi firmware version adapter.setState(`${bridgeId}.info.wifiFirmwareVersion`, {val: bridgeWifiFwVer, ack: true}); } else if (bridgeType == 2) { // set app version adapter.setState(`${bridgeId}.info.appVersion`, {val: bridgeAppVer, ack: true}); } // set timestamp adapter.setState(`${bridgeId}.info.timestamp`, {val: _timestamp, ack: true}); } function setLockState(_nukiId, _deviceType, _nukiState, _firmWare) { let timeStamp = null; if (_nukiState == null) { // no state set return; } // set device type adapter.setState(`${_nukiId}.info.deviceType`, {val: _deviceType, ack: true}); // set battery status adapter.setState(`${_nukiId}.info.batteryCritical`, {val: _nukiState.batteryCritical, ack: true}); if (_nukiState.hasOwnProperty('batteryCharging')) { if (_nukiState.batteryCharging != null) { // set battery charge status adapter.setState(`${_nukiId}.info.batteryCharging`, {val: _nukiState.batteryCharging, ack: true}); } } if (_nukiState.hasOwnProperty('batteryChargeState')) { if (_nukiState.batteryChargeState != null) { // set battery charge level adapter.setState(`${_nukiId}.info.batteryChargeState`, {val: _nukiState.batteryChargeState, ack: true}); } } if (_nukiState.hasOwnProperty('keypadBatteryCritical')) { if (_nukiState.keypadBatteryCritical != null) { // set keypad battery status adapter.setState(`${_nukiId}.info.keypadBatteryCritical`, {val: _nukiState.keypadBatteryCritical, ack: true}); } } // set timestamp if (_nukiState.hasOwnProperty('timestamp')) { timeStamp = _nukiState.timestamp; } else { timeStamp = new Date().toISOString().substr(0,19) + '+00:00'; } adapter.setState(`${_nukiId}.states.timestamp`, {val: timeStamp, ack: true}); // lock or opener? if (_deviceType == 0 || _deviceType == 3 || _deviceType == 4) { // set lock action and state switch(_nukiState.state) { case 1: // fall through case 4: adapter.setState(`${_nukiId}.states.lockState`, {val: false, ack: true}); adapter.setState(`${_nukiId}.actions.lockAction`, {val: false, ack: true}); break; case 2: // fall through case 3: // fall through case 5: // fall through case 6: // fall through case 7: adapter.setState(`${_nukiId}.states.lockState`, {val: true, ack: true}); adapter.setState(`${_nukiId}.actions.lockAction`, {val: true, ack: true}); break; default: adapter.setState(`${_nukiId}.states.lockState`, {val: true, ack: true}); adapter.setState(`${_nukiId}.actions.lockAction`, {val: true, ack: true}); break; } } else if (_deviceType == 2) { // set opener action and state switch(_nukiState.state) { case 1: // fall through case 4: adapter.setState(`${_nukiId}.states.lockState`, {val: false, ack: true}); adapter.setState(`${_nukiId}.actions.rtoAction`, {val: false, ack: true}); break; case 2: // fall through case 3: // fall through case 5: // fall through case 6: // fall through case 7: adapter.setState(`${_nukiId}.states.lockState`, {val: true, ack: true}); adapter.setState(`${_nukiId}.actions.rtoAction`, {val: true, ack: true}); break; default: adapter.setState(`${_nukiId}.states.lockState`, {val: true, ack: true}); adapter.setState(`${_nukiId}.actions.rtoAction`, {val: true, ack: true}); break; } } else { // unknown device return; } // reset action state after delay actionTimeOut = setTimeout(function() { adapter.setState(`${_nukiId}.actions.action`, {val: 0, ack: true}); }, timeOut); // set mode let mode = 0; mode = _nukiState.mode; adapter.setState(`${_nukiId}.info.mode`, {val: mode, ack: true}); // set status let state = 0; state = _nukiState.state; adapter.setState(`${_nukiId}.states.state`, {val: state, ack: true}); if (_nukiState.hasOwnProperty('ringactionState') && _nukiState.ringactionState != null) { // set doorsensor status adapter.setState(`${_nukiId}.states.ringactionState`, {val: _nukiState.ringactionState, ack: true}); } if (_nukiState.hasOwnProperty('ringactionTimestamp') && _nukiState.ringactionTimestamp != '') { // set doorsensor status adapter.setState(`${_nukiId}.states.ringactionTimestamp`, {val: _nukiState.ringactionTimestamp, ack: true}); } if (_nukiState.hasOwnProperty('doorsensorState')) { // set doorsensor status let doorState = 0; doorState = _nukiState.doorsensorState; adapter.setState(`${_nukiId}.states.doorState`, {val: doorState, ack: true}); } if (_firmWare != null && _firmWare != '') { // set firmware version adapter.setState(`${_nukiId}.info.firmwareVersion`, {val: _firmWare, ack: true}); } } function updateAllLockStates(_content, _init) { let obj = null; let deviceType = 0; let nukilock = 0; if (_content == null) { adapter.log.error('no content'); return; } for (nukilock in _content) { obj = _content[nukilock]; if (obj) { if (_init) { adapter.log.debug(`found Nuki device: ${obj.nukiId}`); initNukiDeviceStates(obj); } else { adapter.log.debug(`updating Nuki device: ${obj.nukiId}`); if (obj.hasOwnProperty('deviceType')) { deviceType = obj.deviceType; } else { deviceType = 0; } if (obj.hasOwnProperty('firmwareVersion')) { setLockState(obj.nukiId, deviceType, obj.lastKnownState, obj.firmwareVersion); } else { setLockState(obj.nukiId, deviceType, obj.lastKnownState); } } } } } function getLockState(_nukiId, _forced) { if (_forced) { // retrieve states directly from the device adapter.getState(`${_nukiId}.info.deviceType`, function (err, state) { let deviceType = 0; let lockStateUrl = null; let timeStamp = ''; deviceType = state.val; if (err) { adapter.log.error(err); return; } if (!deviceType || deviceType == 1) { lockStateUrl = `http://${bridgeIp}:${bridgePort}/lockState?nukiId=${_nukiId}&${get_token()}`; } else { lockStateUrl = `http://${bridgeIp}:${bridgePort}/lockState?nukiId=${_nukiId}&deviceType=${deviceType}&${get_token()}`; } request( { url: lockStateUrl, json: true }, function (error, response, content) { let doorsensorState = 4; let doorsensorStateName = 'door state unknown'; let keypadBatteryCritical = null; let ringactionState = null; let ringactionTimestamp = ''; adapter.log.debug(`state requested: ${lockStateUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 401: adapter.log.error('Given token is invalid.'); break; case 404: adapter.log.error('Nuki device is unknown.'); break; case 503: adapter.log.error('Nuki device is offline.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content && content.hasOwnProperty('success')) { if (content.success) { timeStamp = new Date().toISOString().substr(0,19) + '+00:00' ; if (req.body.hasOwnProperty('keypadBatteryCritical')) { keypadBatteryCritical = req.body.keypadBatteryCritical; } if (content.hasOwnProperty("doorsensorState")){ doorsensorState = content.doorsensorState; doorsensorStateName = content.doorsensorStateName; } if (req.body.hasOwnProperty('ringactionState')) { ringactionState = req.body.ringactionState; ringactionTimestamp = req.body.ringactionTimestamp; } let nukiState = { "mode": mode, "state": state, "stateName": stateName, "batteryCritical": batteryCritical, "keypadBatteryCritical": keypadBatteryCritical, "doorsensorState": doorsensorState, "doorsensorStateName": doorsensorStateName, "timestamp": timeStamp, "ringactionState": ringactionState, "ringactionTimestamp": ringactionTimestamp }; setLockState(_nukiId, deviceType, nukiState); } else { adapter.log.warn('State has not been retrieved. Check if device is connected to bridge and try again.'); } } else { adapter.log.warn('Response has no valid content. Check IP address and try again.'); } } ) }); } else { // get all Nuki devices on bridge getLockList(false); } } function setLockAction(_nukiId, _action) { adapter.getState(`${_nukiId}.info.deviceType`, function (err, state) { let deviceType = 0; let lockActionUrl = null; deviceType = state.val; if (err) { adapter.log.error(err); return; } adapter.log.debug(`Setting lock action ${_action} for NukiID ${_nukiId} (device type ${deviceType}).`); if (!deviceType || deviceType == 1) { lockActionUrl = `http://${bridgeIp}:${bridgePort}/lockAction?nukiId=${_nukiId}&action=${_action}&${get_token()}`; } else { lockActionUrl = `http://${bridgeIp}:${bridgePort}/lockAction?nukiId=${_nukiId}&deviceType=${deviceType}&action=${_action}&${get_token()}`; } request( { url: lockActionUrl, json: true }, function (error, response, content) { adapter.log.debug(`action requested: ${lockActionUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 400: adapter.log.error('Given action is invalid.'); break; case 401: adapter.log.error('Given token is invalid.'); break; case 404: adapter.log.error('Nuki device is unknown.'); break; case 503: adapter.log.error('Nuki device is offline.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content && content.hasOwnProperty('success')) { if (!content.success) { adapter.log.warn(`action ${_action} not successfully set!`); } else { adapter.log.info(`action ${_action} set successfully`); if (hostCb == false) { getLockState(_nukiId, false); } else { } } } else { adapter.log.warn('Response has no valid content. Check IP address and try again.'); } } ) }); } function getBridgeList() { let bridgeListUrl = 'https://api.nuki.io/discover/bridges'; let obj = null; request( { url: bridgeListUrl, json: true }, function (error, response, content) { adapter.log.debug(`Bridge list requested: ${bridgeListUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { adapter.log.error(`HTTP-response: ${response.statusCode}`); return; } if (!content || !content.hasOwnProperty('errorCode')) { adapter.log.warn('Response has no valid content. Check if bridge ist pluged in and active and try again.'); return; } if (content.errorCode != 0) { adapter.log.warn('Bridge respose has not been retrieved. Check if bridge ist pluged in and active and try again.'); return; } for (let bridge in content.bridges) { obj = content.bridges[bridge]; if (!obj) { adapter.log.warn('Bridge respose has not been retrieved. Check if bridge ist plugged in and active and try again.'); return; } if (obj.hasOwnProperty('ip')) { if (obj.ip == bridgeIp) { // found bridge bridgeId = obj.bridgeId; bridgeType = 1; if (obj.port == bridgePort) { // correct port adapter.log.info(`found hardware bridge: ${bridgeId} (IP: ${bridgeIp}; Port: ${bridgePort})`); } else { // different port adapter.log.warn(`found hardware bridge (ID: ${bridgeId}; IP: ${obj.ip}) has different port than specified! (specified: ${bridgePort}; actual: ${obj.port}). Please specify correct port of bridge.`); } } else if (obj.ip == '0.0.0.0' || obj.ip == '') { adapter.log.warn(`bridgeID ${obj.bridgeId}: no auto discovery possible. Has the HTTP API been activated and the token been set?`); } else { adapter.log.info(`found another hardware bridge: ${obj.bridgeId} (IP: ${obj.ip}; Port: ${obj.port})`); } } else { // software bridge: doesn't come with IP if (bridgeId == '' || bridgeId == null) { bridgeId = obj.bridgeId; bridgeType = 2; } adapter.log.info(`found software bridge: ${obj.bridgeId}`); } } if (bridgeId == '' || bridgeId == null) { adapter.log.error('no bridge has been found'); return; } } ) } function getBridgeInfo(_init, _encrypt) { let bridgeInfoUrl = `http://${bridgeIp}:${bridgePort}/info?${get_token(_encrypt)}`; if (adapter.config.bridge_ip == '' || adapter.config.bridge_port == '') { adapter.log.warn('please specify IP and port of bridge'); return; } request( { url: bridgeInfoUrl, json: true }, function (error, response, content) { adapter.log.info(`Bridge Info requested: ${bridgeInfoUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 401: adapter.log.error('Given token is invalid.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content) { let ids = content.ids; let versions = content.versions; bridgeType = content.bridgeType; bridgeId = ids.serverId; if (bridgeType == 1) { bridgeHwId = ids.hardwareId.toString(); bridgeFwVer = versions.firmwareVersion; bridgeWifiFwVer = versions.wifiFirmwareVersion; } else { bridgeAppVer = versions.appVersion } } else { adapter.log.error('Unable access the bridge with specified IP address and port.'); // Nuki bridge discovery getBridgeList(); return; } if (_init) { initBridgeStates(bridgeName, bridgeToken); } else { setBridgeState(content.currentTime); } } ) } async function getLockList(_init) { // delay before next request await sleep(timeOut); // get Nuki bridge info getBridgeInfo(_init, 'X'); if (!bridgeFwVer) { // delay before next request await sleep(timeOut); // get Nuki bridge info getBridgeInfo(_init, ''); } // delay before next request await sleep(timeOut); let lockListUrl = `http://${bridgeIp}:${bridgePort}/list?${get_token()}`; request( { url: lockListUrl, json: true }, function (error, response, content) { adapter.log.info(`Lock list requested: ${lockListUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 401: adapter.log.error('Given token is invalid.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content) { updateAllLockStates(content, _init); } else { adapter.log.warn('Response has no valid content. Check IP address and port and try again.'); } } ) if (_init) { // delay before next request await sleep(timeOut); // check for callbacks on Nuki bridge checkCallback(hostCb); } } function initServer(_ip, _port) { app.use(bodyParser.json()); // support json encoded bodies app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies // routes will go here app.get('/api/:key', function(req, res) { res.send(`Hello ${req.params.key} ;-)`); }); // POST parameters sent with app.post(`/api/nuki.${adapter.instance}`, function(req, res) { let nukiId = req.body.nukiId; let deviceType = 0; let mode = '2'; let state = req.body.state; let stateName = req.body.stateName; let batteryCritical = req.body.batteryCritical; let keypadBatteryCritical = null; let timeStamp = new Date().toISOString().substr(0,19) + '+00:00'; let doorsensorState = 4; let doorsensorStateName = 'door state unknown'; let ringactionState = null; let ringactionTimestamp = ''; if (req.body.hasOwnProperty('keypadBatteryCritical')) { keypadBatteryCritical = req.body.keypadBatteryCritical; } if (req.body.hasOwnProperty('deviceType')) { deviceType = req.body.deviceType } if (req.body.hasOwnProperty('mode')) { mode = req.body.mode; } if (req.body.hasOwnProperty('doorsensorState')) { doorsensorState = req.body.doorsensorState; doorsensorStateName = req.body.doorsensorStateName; } if (req.body.hasOwnProperty('ringactionState')) { ringactionState = req.body.ringactionState; ringactionTimestamp = req.body.ringactionTimestamp; } let nukiState = { "mode": mode, "state": state, "stateName": stateName, "batteryCritical": batteryCritical, "keypadBatteryCritical": keypadBatteryCritical, "doorsensorState": doorsensorState, "doorsensorStateName": doorsensorStateName, "timestamp": timeStamp, "ringactionState": ringactionState, "ringactionTimestamp": ringactionTimestamp }; try { adapter.log.info(`status change received for NukiID ${nukiId}: ${nukiState.stateName}`); adapter.log.info(`battery status received for NukiID ${nukiId}: ${nukiState.batteryCritical}`); setLockState(nukiId, deviceType, nukiState); res.sendStatus(200); } catch (e) { res.sendStatus(500); adapter.log.warn(e.message); } }); // start the server app.listen(_port, _ip); adapter.log.info(`Server listening to http://${_ip}:${_port}`); } function checkCallback(_hostCb) { let cbListUrl = `http://${bridgeIp}:${bridgePort}/callback/list?&${get_token()}`; let cbUrl = `http://${hostIp}:${hostPort}/api/nuki.${adapter.instance}`; let cbExists = false; let cbId = null; request( { url: cbListUrl, json: true }, function (error, response, content) { adapter.log.debug(`Callback list requested: ${cbListUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 401: adapter.log.error('Given token is invalid.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content && content.hasOwnProperty('callbacks')) { for (let row in content.callbacks) { cbId = content.callbacks[row]; if (cbId.url == cbUrl) { cbExists = true; if (_hostCb == false) { adapter.log.debug(`Callback should be removed: ${cbUrl}`); removeCallback(cbId.id); } } } if (_hostCb == true) { if (cbId) { callbackId = cbId.id; } else { callbackId = '0'; } if (cbExists) { cbSet = true; adapter.log.info(`Callback allready set: ${cbUrl}`); initServer(hostIp, hostPort); } else { if (callbackId == '3') { cbSet = false; adapter.log.warn('Too many Callbacks defined (3). First delete at least 1 Callback on your Nuki bridge.'); } else { cbSet = true; initServer(hostIp, hostPort); setCallback(cbUrl); } } } } else { adapter.log.warn('Response has no valid content. Check IP address and try again.'); } } ) } async function removeCallback(_id) { let callbackRemoveUrl = `http://${bridgeIp}:${bridgePort}/callback/remove?id=${_id}&${get_token()}`; if (hostCb == false) { // delay before next request await sleep(timeOut); request( { url: callbackRemoveUrl, json: true }, function (error, response, content) { adapter.log.debug(`Callback removal requested: ${callbackRemoveUrl}`); if (error) { adapter.log.error(error); return; } if (response.statusCode != 200) { switch (response.statusCode) { case 400: adapter.log.error('Given url is invalid or too long.'); break; case 401: adapter.log.error('Given token is invalid.'); break; default: adapter.log.error(`HTTP-response: ${response.statusCode}`); break; } return; } if (content && content.hasOwnProperty('success')) { if (content.success) { cbSet = false; adapter.log.info(`Callback-ID successfully removed: ${_id}`); } else { adapter.log.warn(`Callback-ID could not be removed: ${_id}`); if (content.hasOwnProperty('message')) { adapter.log.warn(content.message); } } } else { adapter.log.warn('Response has no valid content. Check IP address and try again.'); } } ) } } async function setCallback(_url) { let callbackString = _url.replace(':', '%3A'); callbackString = callbackString.replace('/', '%2F'); let callbackAddUrl = `http://${bridgeIp}:${bridgePort}/callback/add?url=${callbackString}&${get_token()}`; if (hostCb == true) { // delay before next request await sleep(timeOut); request( { url: callbackAddUrl, json: true }, function (error, response, content) { adapter.log.debug(`Callback requested: ${callbackAddUrl}`); if (error) { adapter.log.error(error);