UNPKG

iobroker.openhab

Version:
864 lines (784 loc) 32.2 kB
/** * * openhab adapter Copyright 2017-2018, bluefox <dogafox@gmail.com> * */ /* jshint -W097 */ /* jshint strict:false */ /* jslint node: true */ 'use strict'; // you have to require the utils module and call adapter function const utils = require('@iobroker/adapter-core'); // Get common adapter utils const request = require('request'); const adapter = new utils.Adapter('openhab'); const ohTypes = require('./lib/types.js'); const rooms = require('./lib/rooms.js'); const funcs = require('./lib/functions.js'); const EventSource = require('eventsource'); let client; const objects = {}; const states = []; let connected = false; let connectingTimeout = null; let URL; let es; let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start // is called when adapter shuts down - callback has to be called under any circumstances! adapter.on('unload', callback => { try { adapter.setState && adapter.setState('info.connection', false, true); client && client.disconnect(); client = null; adapter.log.info('cleaned everything up...'); callback(); } catch (e) { callback(); } }); function RGBToHSB(rgb) { const hsb = { h: 0, s: 0, b: 0 }; const min = Math.min(rgb.r, rgb.g, rgb.b); const max = Math.max(rgb.r, rgb.g, rgb.b); const delta = max - min; hsb.b = max; if (max) { } hsb.s = max ? 255 * delta / max : 0; if (hsb.s) { if (rgb.r === max) { hsb.h = (rgb.g - rgb.b) / delta; } else if (rgb.g === max) { hsb.h = 2 + (rgb.b - rgb.r) / delta; } else { hsb.h = 4 + (rgb.r - rgb.g) / delta; } } else { hsb.h = -1; } hsb.h *= 60; if (hsb.h < 0) { hsb.h += 360; } hsb.s *= 100 / 255; hsb.b *= 100 / 255; return hsb; } function HSBToRGB(h1, s1, b1) { const rgb = {}; let h = Math.round(h1); const s = Math.round(s1 * 255 / 100); const v = Math.round(b1 * 255 / 100); if (!s) { rgb.g = v; rgb.b = v; rgb.r = v; } else { const t1 = v; const t2 = (255 - s) * v / 255; const t3 = (t1 - t2) * (h % 60) / 60; if (h === 360) { h = 0; } if (h < 60) { rgb.r = t1; rgb.b = t2; rgb.g = t2 + t3 } else if (h < 120) { rgb.g = t1; rgb.b = t2; rgb.r = t1 - t3 } else if (h < 180) { rgb.g = t1; rgb.r = t2; rgb.b = t2 + t3 } else if (h < 240) { rgb.b = t1; rgb.r = t2; rgb.g = t1 - t3 } else if (h < 300) { rgb.b = t1; rgb.g = t2; rgb.r = t2 + t3 } else if (h < 360) { rgb.r = t1; rgb.g = t2; rgb.b = t1 - t3 } else { rgb.r = 0; rgb.g = 0; rgb.b = 0 } } return {r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b)}; } function RGBToHex(rgb) { const hex = [ rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16) ]; if (hex[0].length === 1) { hex[0] = '0' + hex[0]; } if (hex[1].length === 1) { hex[1] = '0' + hex[1]; } if (hex[2].length === 1) { hex[2] = '0' + hex[2]; } return hex.join(''); } function HexToRGB(hex) { hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; } // type : the type received from OH // val : value // destType : destination type (needed eg. for OH color type) function oh2iob(type, val, destType, oldValue) { if (undefined === type || type === null) { type = 'undefined'; } type = type.toString().toLowerCase(); // get rid of capital letters. Makes it easy to parse. if (undefined === destType || destType === null) { destType = 'undefined'; } destType = destType.toString().toLowerCase(); // get rid of capital letters. Makes it easy to parse. if (type === 'booleantype' || type === 'boolean') { return (val === true || val === 'true' || val === '1' || val === 1 || val === 'on' || val === 'ON') } else if (type === 'decimaltype' || type === 'decimal' || type.startsWith('number') || type === 'dimmer' || type === 'rollershutter' || type === 'quantity') { // OH2.4 allows to add additional information to the number type. Therefore the check for NUMBER is done differently. // Quantity is a number having a unit of measure return parseFloat((val || '0').toString().replace(',', '.')); } else if (type === 'onofftype' || type === 'onoff' || type === 'switch') { if (destType === 'color') { let brightness = 0; if (val === 'ON' || val === 'on') { brightness = 100; } // RGB to HSB let rgbValue = HexToRGB(oldValue); const hsbValue = RGBToHSB(rgbValue); rgbValue = HSBToRGB(parseFloat(hsbValue.h), parseFloat(hsbValue.s), parseFloat(brightness)); return '#' + RGBToHex(rgbValue); } else if (destType === 'dimmer') { if (val === 'ON' || val === 'on') { return 100; } else { return 0; } } else { return (val === 'ON' || val === 'on'); } } else if (type === 'openclosedtype' || type === 'openclosed' || type === 'contact') { return (val === 'OPEN' || val === 'open'); } else if (type === 'percenttype' || type === 'percent') { if (destType === 'color') { // Percent type means to scale the brightness // RGB to HSB let rgbValue = HexToRGB(oldValue); const hsbValue = RGBToHSB(rgbValue); rgbValue = HSBToRGB(parseFloat(hsbValue.h), parseFloat(hsbValue.s), parseFloat(val)); return '#' + RGBToHex(rgbValue); } else { return parseFloat(val); } } else if (type === 'stringtype' || type === 'stringlist' || type === 'string' || type === 'location' || type === 'call') { // only workaround for Openhab window handle (tri state) if (val === 'OPEN') { return 'open'; } else if (val === 'CLOSED') { return 'closed'; } else if (val === 'AJAR') { return 'tilt'; } else { return val; } } else if (type === 'datetimetype' || type === 'datetime') { // do nothing // 2017-05-05T03:28:00.000+0000 return val; } else if (type === 'hsbtype' || type === 'hsb' || type === 'color') { // do nothing // 336,17,37 // make one RGB value out of it to have only one variable. The RGB value can be used for a colorpicker. const hsl = val.toString().split(','); const rgbValue = HSBToRGB(parseFloat(hsl[0]), parseFloat(hsl[1]), parseFloat(hsl[2])); return '#' + RGBToHex(rgbValue); } else if (type === 'updown') { // Shutter command to move up or down. The destination type of the related object is level.blind and needs a percentage value. // Therefore this type is simply ignored and return the original value of the object return oldValue; } else { adapter.log.warn('oh2iob - Unknown type: ' + type); return val; } } function iob2oh(type, val) { if (undefined === type || type === null) { type = 'undefined'; } type = type.toString().toLowerCase(); // get rid of capital letters. Makes it easy to parse. if (type === 'switch') { if (val === true || val === 'true' || val === 1 || val === '1' || val === 'ON' || val === 'on') { return 'ON'; } else { return 'OFF'; } } else if (type.startsWith('number')) { return parseFloat(val); } else if (type === 'dimmer' || type === 'rollershutter') { return parseInt(val); } else if (type === 'contact') { // not yet supported by Openhab, but maybe in the future if (val === true || val === 'true' || val === 1 || val === '1' || val === 'OPEN' || val === 'open') { return 'OPEN'; } else { return 'CLOSED'; } } else if (type === 'string') { // Writing of window handle is not supported return val; } else if (type === 'color') { // RGB to HSB const rgbValue = HexToRGB(val.toString()); const hsbValue = RGBToHSB(rgbValue); return (hsbValue.h.toString() + ',' + hsbValue.s.toString() + ',' + hsbValue.b.toString()); } else { adapter.log.warn('iob2oh - Unknown type: ' + type + ' with value = ' + val); return val; } } // is called if a subscribed state changes adapter.on('stateChange', (id, state) => { adapter.log.debug('stateChange ' + id + ' ' + JSON.stringify(state)); if (state && !state.ack) { if (objects[id]) { if (objects[id].common.write && objects[id].native.name) { states[id] = state.val; if (!connected) { adapter.log.warn('Cannot control: no connection to openhab "' + adapter.config.host + '"'); } else { adapter.log.debug('Type = ' + objects[id].native.type); const originalVal = state.val; if (state.val === null || state.val === undefined) { state.val = ''; } else { // convert values state.val = iob2oh(objects[id].native.type, state.val); } const link = URL + '/items/' + objects[id].native.name; adapter.log.debug(link); request.post({ headers: {'content-type': 'text/plain'}, url: link, body: state.val.toString() }, (err, res, body) => { if (err || res.statusCode !== 200) { adapter.log.warn('Cannot write "' + id + '": ' + (body || err || res.statusCode)); adapter.setForeignState(id, {val: originalVal, ack: true, q: 0x40}); } else { adapter.setForeignState(id, {val: originalVal, ack: true, q: 0x0}); } }); } } else { adapter.log.warn('State "' + id + '" is read only'); } } else { adapter.log.warn('Unknown state "' + id + '"'); } } }); // Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ... adapter.on('message', 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', () => { // Generate secret for session manager if (adapter.config.username) { adapter.getForeignObject('system.config', (err, obj) => { if (!err && obj) { if (!obj.native || !obj.native.secret) { obj.native = obj.native || {}; require('crypto').randomBytes(24, (ex, buf) => { secret = buf.toString('hex'); adapter.extendForeignObject('system.config', {native: {secret: secret}}); main(); }); } else { adapter.config.password = decrypt(obj.native.secret, new Buffer(adapter.config.password, 'base64').toString('binary')); main(); } } else { adapter.logger.error('Cannot find object system.config'); } }); } else { main(); } }); function decrypt(key, value) { let result = ''; for (let i = 0; i < value.length; i++) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); } return result; } function syncObjects(objs, callback) { if (!objs || !objs.length) { callback && callback(); return; } const obj = objs.shift(); adapter.getForeignObject(obj._id, (err, oObj) => { if (!oObj) { objects[obj._id] = obj; adapter.setForeignObject(obj._id, obj, () => { setTimeout(syncObjects, 0, objs, callback); }); } else { let changed = false; for (const a in obj.common) { if (obj.common.hasOwnProperty(a) && oObj.common[a] !== obj.common[a]) { changed = true; oObj.common[a] = obj.common[a]; } } if (JSON.stringify(obj.native) !== JSON.stringify(oObj.native)) { changed = true; oObj.native = obj.native; } objects[obj._id] = oObj; if (changed) { adapter.setForeignObject(oObj._id, oObj, () => setTimeout(syncObjects, 0, objs, callback)); } else { setTimeout(syncObjects, 0, objs, callback); } } }); } function syncStates(_states, callback) { if (!_states || !_states.length) { callback && callback(); return; } const state = _states.shift(); adapter.getForeignState(state._id, (err, oState) => { if (!oState) { adapter.setForeignState(state._id, state.val, () => setTimeout(syncStates, 0, _states, callback)); } else { let changed = false; for (const a in state.val) { if (state.val.hasOwnProperty(a) && (typeof state.val[a] !== 'object' && state.val[a] !== oState[a]) || (typeof state.val[a] === 'object' && JSON.stringify(state.val[a]) !== JSON.stringify(oState[a]))) { changed = true; oState[a] = state.val[a]; } } if (changed) { adapter.setForeignState(state._id, oState, () => setTimeout(syncStates, 0, _states, callback)); } else { setTimeout(syncStates, 0, _states, callback); } } }); } /* function syncDevices(devices, callback) { const objs = []; const _states = []; for (const d = 0; d < devices.length; d++) { const localObjects = []; const device = devices[d]; const obj = { _id: adapter.namespace + '.devices.' + device.id, common: { name: device.name }, type: 'channel' }; objs.push(obj); const attributes = device.attributes; if ((!attributes || !attributes.length) && device.config) attributes = device.config.attributes; if (attributes && attributes.length) { for (const a = 0; a < attributes.length; a++) { const attr = attributes[a]; const id = adapter.namespace + '.devices.' + device.id + '.' + attr.name.replace(/\s/g, '_'); obj = { _id: id, common: { name: device.name + ' - ' + (attr.acronym || attr.name), desc: attr.description, type: attr.type, read: true, write: false, unit: attr.unit === 'c' ? '°C' : (attr.unit === 'f' ? '°F' : attr.unit) //role: acronym2role(attr.acronym) }, native: { }, type: 'state' }; _states.push({ _id: id, val: { ack: true, val: attr.value, ts: attr.lastUpdate } }); states[id] = attr.value; delete attr.value; delete attr.lastUpdate; delete attr.history; obj.native = attr; if (obj.common.type === 'boolean') { if (device.template === 'presence') obj.common.role = 'state';//'indicator.presence'; if (attr.labels && attr.labels[0] !== 'true') { obj.common.states = {false: attr.labels[1], true: attr.labels[0]}; } } else if (obj.common.type === 'number') { if (obj.common.unit === '°C' || obj.common.unit === '°F') { obj.common.role = 'value.temperature'; } else if (obj.common.unit === '%') { obj.common.min = 0; obj.common.max = 100; // Detect if temperature exists const found = false; for (const k = 0; k < localObjects.length; k++) { if (localObjects[k].common.unit === '°C' || localObjects[k].common.unit === '°F') { found = true; break; } } if (found) { obj.common.role = 'value.humidity'; } } if (attr.name === 'latitude') { obj.common.role = 'value.gps.latitude'; } else if (attr.name === 'longitude') { obj.common.role = 'value.gps.longitude'; } if (attr.name === 'gps') { obj.common.role = 'value.gps'; } } else { if (attr.name === 'battery') { obj.common.role = 'indicator.battery'; obj.native.mapping = {'ok': false, 'low': true}; obj.common.type = 'boolean'; obj.common.states = {false: 'ok', true: 'low'}; attr.value = (attr.value !== 'ok'); } } if (attr.enum && !obj.common.states) { obj.common.states = {}; for (const e = 0; e < attr.enum.length; e++) { if (attr.enum[e] === 'manu') { obj.common.states.manu = 'manual'; } else if (attr.enum[e] === 'auto') { obj.common.states.auto = 'automatic'; } else{ obj.common.states[attr.enum[e]] = attr.enum[e]; } } } objs.push(obj); localObjects.push(obj); } } const actions = device.actions; if ((!actions || !actions.length) && device.config) actions = device.config.actions; if (actions && actions.length) { for (const c = 0; c < actions.length; c++) { const action = actions[c]; for (const p in action.params) { if (!action.params.hasOwnProperty(p)) continue; // try to find state for that const _found = false; for (const u = 0; u < localObjects.length; u++) { if (localObjects[u].native.name === p) { _found = true; obj = localObjects[u]; obj.native.control = { action: action.name, deviceId: device.id }; obj.common.write = true; if (obj.common.role === 'value.temperature') obj.common.role = 'level.temperature'; } } if (!_found) { obj = { _id: adapter.namespace + '.devices.' + device.id + '.' + action.name.replace(/\s/g, '_') + '.' + p.replace(/\s/g, '_'), common: { desc: action.params[p].description || action.description, name: device.name + ' - ' + action.name + '.' + p, read: false, write: true, type: action.params[p].type }, native: { name: p, control: { action: action.name, deviceId: device.id } }, type: 'state' }; objs.push(obj); } } } } } const ids = []; for (const j = 0; j < objs.length; j++) { ids.push(objs[j]._id); objects[objs[j]._id] = objs[j]; } syncObjects(objs, function () { syncStates(_states, function () { callback && callback(ids); }); }); } function syncGroups(groups, ids, callback) { const enums = []; const obj = { _id: 'enum.openhab', common: { members: [], name: 'openhab groups' }, native: {}, type: 'enum' }; enums.push(obj); for (const g = 0; g < groups.length; g++) { obj = { _id: 'enum.openhab.' + groups[g].id, type: 'enum', common: { name: groups[g].name, members: [] }, native: {} }; for (const m = 0; m < groups[g].devices.length; m++) { const id = adapter.namespace + '.devices.' + groups[g].devices[m].replace(/\s/g, '_'); if (ids.indexOf(id) === -1) { // try to find const found = false; const _id = id.toLowerCase(); for (const i = 0; i < ids.length; i++) { if (ids[i].toLowerCase() === _id) { id = ids[i]; found = true; break; } } if (found) { obj.common.members.push(id); } else { adapter.log.warn('Device "' + groups[g].devices[m] + '" was found in the group "' + groups[g].name + '", but not found in devices'); } } else { obj.common.members.push(id); } } enums.push(obj); } syncObjects(enums, callback); } */ function connect(callback) { connectingTimeout = null; let auth; if (adapter.config.username) { auth = { auth: { user: adapter.config.username, pass: adapter.config.password, sendImmediately: false } }; } request.get(URL + '/items?recursive=false', auth, (err, resp, body) => { if (body) { updateConnected(true); try { const items = JSON.parse(body); const enums = {}; const _states = []; const objs = []; let id; for (let i = 0; i < items.length; i++) { adapter.log.debug('OH item to be parsed ' + JSON.stringify(items[i])); if (items[i].type === 'Group') { let groupId = 'enum.openhab.' + items[i].name; if ((items[i].label && rooms.indexOf(items[i].label.toLowerCase()) !== -1) || (items[i].name && rooms.indexOf(items[i].name.toLowerCase()) !== -1)) { groupId = 'enum.rooms.' + items[i].name; } else if ((items[i].label && funcs.indexOf(items[i].label.toLowerCase()) !== -1) || (items[i].name && funcs.indexOf(items[i].name.toLowerCase()) !== -1)) { groupId = 'enum.functions.' + items[i].name; } if (enums[items[i].name]) { enums[items[i].name].common.name = items[i].label; enums[items[i].name]._id = groupId; } else { enums[items[i].name] = { _id: groupId, common: { name: items[i].label, members: [] }, native: {}, type: 'enum' }; objs.push(enums[items[i].name]); } } else { // OH feature "Number:Temperature" => split and handle the main part adapter.log.debug("Type to be handled : " + items[i].type.split(':')[0]); const common = ohTypes[items[i].type.split(':')[0]] ? ohTypes[items[i].type.split(':')[0]](items[i]) : { type: 'string', role: 'state', name: items[i].label }; adapter.log.debug("Common object: " + JSON.stringify(common)); id = adapter.namespace + '.items.' + items[i].name; objs.push({ _id: id, common: common, native: { name: items[i].name, type: items[i].type }, type: 'state' }); // insert to enums if (items[i].groupNames && items[i].groupNames.length) { for (let e = 0; e < items[i].groupNames.length; e++) { const group = items[i].groupNames[e]; if (!enums[group]) { enums[group] = { _id: 'enum.openhab.' + group, common: { members: [] }, native: {}, type: 'enum' }; objs.push(enums[group]); } enums[group].common.members.push(id); } } if (typeof items[i].state !== 'undefined' && items[i].state !== 'undefined') { const iobStateVal = oh2iob(items[i].type, items[i].state, null, null); states[id] = iobStateVal; _states.push({ _id: id, val: {val: iobStateVal, ack: true} }); } } } syncObjects(objs, () => { syncStates(_states, callback); if (adapter.config.username) { URL = URL.replace(/^http:\/\//, 'http://' + adapter.config.username + ':' + adapter.config.password + '@'); URL = URL.replace(/^https:\/\//, 'https://' + adapter.config.username + ':' + adapter.config.password + '@'); } es = new EventSource(URL + '/events'); es.addEventListener('message', (eventPayload) => { const event = JSON.parse(eventPayload.data); if (event.type === 'ItemStateEvent') { // smarthome/items/GEG_HZ_Soll/state const parts = event.topic.split('/'); const topic = parts[2]; const value = JSON.parse(event.payload); const id = adapter.namespace + '.items.' + topic; value.value = oh2iob(value.type, value.value, objects[id].native.type, states[id]); adapter.log.debug('Received [' + id + '] = ' + JSON.stringify(value)); states[id] = value.value; adapter.setState(id, value.value, true); } }); es.onerror = err => { if (err) { if (err.status === 401 || err.status === 403) { adapter.log.error('not authorized'); } else { adapter.log.debug('Error: ' + JSON.stringify(err)); } } }; }); } catch (e) { updateConnected(false); adapter.log.error('Invalid answer on "' + URL + '/items?recursive=false": cannot parse response: ' + e); connectingTimeout = setTimeout(connect, adapter.config.reconnectTimeout); callback && callback(); } } else { updateConnected(false); adapter.log.error('Cannot get answer from "' + URL + '/items?recursive=false": ' + err); connectingTimeout = setTimeout(connect, adapter.config.reconnectTimeout); callback && callback(); } }); } function updateConnected(isConnected) { if (connected !== isConnected) { connected = isConnected; adapter.setState('info.connection', connected, true); adapter.log.info(isConnected ? 'connected' : 'disconnected'); if (!isConnected && es) { es.close(); es = null; } } } function main() { if (adapter.config.url) { adapter.config.protocol = adapter.config.url.match(/^https:/) ? 'https' : 'http'; const url = adapter.config.url.replace('https://', '').replace('http://', ''); let parts = url.split('/'); adapter.config.path = '/' + (parts[1] || ''); parts = parts[0].split(':'); adapter.config.host = parts[0]; adapter.config.port = parts[1] || 80; delete adapter.config.url; } if (adapter.config.host) { URL = adapter.config.protocol + '://' + adapter.config.host + ':' + (adapter.config.port || 80) + adapter.config.path; if (URL[URL.length - 1] === '/') { URL = URL.substring(0, URL.length - 1); } } else { adapter.log.warn('No REST API URL defined.'); return; } adapter.config.reconnectTimeout = parseInt(adapter.config.reconnectTimeout, 10) || 30000; adapter.setState('info.connection', false, true); connect(); // in this openhab all states changes inside the adapters namespace are subscribed adapter.subscribeStates('*'); }