iobroker.openhab
Version:
864 lines (784 loc) • 32.2 kB
JavaScript
/**
*
* openhab adapter Copyright 2017-2018, bluefox <dogafox@gmail.com>
*
*/
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
;
// 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('*');
}