iobroker.lightify
Version:
OSRAM Lightify Adapter
738 lines (642 loc) • 25.4 kB
JavaScript
'use strict';
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// patch node-lightify to get & set the mac-value as hex string
Buffer.prototype.writeDoubleLE = function (val /*, pos*/) {
return this.write(val.toLowerCase(), 0, 8, 'hex');
};
Buffer.prototype.readDoubleLE = function (pos, len) {
return this.toString('hex', pos, len === undefined ? pos+8 : pos + len).toUpperCase();
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var soef = require('soef');
// BF colorsModule unused
var colorsModule = require(__dirname + '/lib/colors');
var Lightify = require(__dirname + '/lib/lightify');//require('node-lightify-soef');
var net;
var lightify;
var types = {};
var connected = false;
function setConnectionState(val) {
if (connected === val) return;
adapter.setState('info.connection', val, true);
connected = val;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// var Lightify = function() {
// if (!(this instanceof Lightify)) {
// return new Lightify();
// }
// if (adapter.common.loglevel !== 'debug') ourlog = undefined; //.debug = function() {};
// lightifyModule.lightify.call(this, adapter.config.ip, undefined); //ourlog);
//
// this.setAutoCloseConnection(true);
// };
// util.inherits(Lightify, lightifyModule.lightify);
//
// Lightify.prototype.isGroupCommand = function (cmdId, body) {
// if (this.groupCommands.indexOf(cmdId) >= 0) {
// //group ids = group number + 7 x 0, so if the bytes 1..7 are 0, it is a group id.
// for (var i = 1; i < 8 && body[i] == 0; i++);
// if (i==8) return 2;
// }
// return 0;
// };
var updateTimer = new soef.Timer();
var refreshTimer = new soef.Timer();
var adapter = soef.Adapter(
main,
onStateChange,
onUnload,
onUpdate,
{ name: 'lightify' }
);
adapter.on('message', function (obj) {
if (!obj || !obj.callback) return;
switch (obj.command) {
case 'browse':
browse(function (list) {
adapter.sendTo(obj.from, obj.command, list, obj.callback);
});
break;
}
});
function closeAll() {
updateTimer.clear();
refreshTimer.clear();
//close socket in node-lightify
if (lightify && lightify.dispose) {
lightify.dispose();
lightify = null;
}
}
function onUnload(callback) {
closeAll();
callback();
}
function onUpdate(oldVersion, newVersion, callback) {
if (oldVersion < 22) {
soef.njs.removeAllObjects(adapter, callback);
return;
}
callback();
}
function getState(id) {
var o = devices.get(id);
if (o === undefined) return undefined;
return o.val || 0;
}
// BF Function unused
function getBrightness(co) {
//var bri = (co.r * 299 + co.g * 587 + co.b*114) / 2560;
var bri = (co.r + co.g + co.b) * 100 / (256 + 256 + 256);
return Math.round(bri);
}
function parseHexColors(val) {
val = val.toString();
var ar = val.split('.');
if (ar && ar.length > 1) {
val = ar[0];
}
if (val[0] === '#') {
val = val.substr(1);
}
var co = {
r: parseInt(val.substr(0, 2), 16),
g: parseInt(val.substr(2, 2), 16) || 0,
b: parseInt(val.substr(4, 2), 16) || 0 //,
};
if (val.length > 7) {
co.w = parseInt(val.substr(6, 2), 16);
}
if (ar && ar.length > 1) {
var m = Number('.' + ar[1]);
for (var i in co) {
co[i] *= m;
}
colorsModule.roundRGB(co);
}
return co;
}
function toArr(v) {
if (Array.isArray(v)) return v;
if (typeof v === 'object') {
var ar = [];
Object.keys(v).forEach(function(n) {
ar.push(v[n]);
});
return ar;
}
return [ v ];
}
function onStateChange(id, state) {
var ar = id.split('.');
var deviceName = ar[2];
var stateName = ar[3];
var o = devices.get(deviceName);
if (o === undefined || o.native === undefined || !o.native.mac) {
if (deviceName === 'refresh') {
createAll(function () {
setTimeout(updateDevices, 1000);
});
return;
}
adapter.log.error('Unknown device ' + deviceName);
return;
}
refreshTimer.clear();
var mac = o.native.mac;
var transitionTime = getState(soef.njs.dcs(deviceName, 'trans')) || 3;
function aktStates() {
return {
r: getState(soef.njs.dcs(deviceName, 'r')),
g: getState(soef.njs.dcs(deviceName, 'g')),
b: getState(soef.njs.dcs(deviceName, 'b')),
sat: getState(soef.njs.dcs(deviceName, 'sat'))
};
}
function checkUpdate () {
// not necessary, will be done at the end of this function
// if (!isGroupId(mac) && (transitionTime / 10) < adapter.config.interval) {
// setTimeout(updateDevices, transitionTime * 100, mac);
// }
}
function setStateWithAck(val) {
if (val === undefined) val = state.val;
devices.setrawval(id, val);
adapter.setState(id, val, true);
}
devices.invalidate(id);
switch (stateName) {
case usedStateNames.transition.n:
setStateWithAck();
// go through all devices and set trans to this value
//if (id === adapter.namespace + '.' + groupIdAll + '.' + usedStateNames.transition.n) {
if (o.native && o.native.devices) {
toArr(o.native.devices).forEach(function(did) {
var fullId = dcs(did, 'trans');
devices.setrawval(fullId, state.val);
adapter.setState(fullId, state.val, true);
});
} else if (mac === groupIdAll) {
devices.foreach('*.trans', function (id) {
devices.setrawval(id, state.val);
adapter.setState(id, state.val, true);
});
}
break;
case 'refresh':
updateDevices(mac);
setStateWithAck(false);
break;
case 'rgbw':
case 'rgb':
var colors = parseHexColors(state.val);
colors.sat = colors.w !== undefined ? colors.w : getState(soef.njs.dcs(deviceName, 'sat'));
//var bri = getBrightness(colors);
//colors.sat = 0x80;
lightify.nodeColor(mac, colors.r, colors.g, colors.b, colors.sat, transitionTime).then(checkUpdate).catch(onError);
//lightify.nodeBrightness(mac, bri, 0).catch(onError);
break;
case 'on':
lightify.nodeOnOff(mac, !!(state.val >> 0)).then(function () {
if (isGroupId(mac)) {
setStateWithAck();
} else {
checkUpdate();
}
}).catch(onError);
break;
case 'softOn':
lightify.nodeSoftOnOff(mac, !!state.val, transitionTime).then(function(data) {
}).catch(onError);
break;
case 'r':
case 'g':
case 'b':
case 'sat':
var ccolors;
if (typeof state.val === 'string' && state.val[0] === '#') {
ccolors = parseHexColors(state.val);
ccolors.sat = ccolors.w !== undefined ? ccolors.w : getState(soef.njs.dcs(deviceName, 'sat'));
} else {
ccolors = aktStates();
ccolors[stateName] = state.val >> 0;
}
lightify.nodeColor(mac, ccolors.r, ccolors.g, ccolors.b, ccolors.sat, transitionTime).then(checkUpdate).catch(onError);
break;
case 'bri':
lightify.nodeBrightness(mac, state.val >> 0, transitionTime).then(function () {
if (isGroupId(mac)) {
setStateWithAck();
if (state.val >> 0) {
adapter.setState(groupIdAll + '.on', true, true);
} else {
//adapter.setState(groupIdAll + '.on', false, true);
}
} else {
checkUpdate();
}
}).catch(onError);
break;
case 'ct':
lightify.nodeTemperature(mac, state.val >> 0, transitionTime).then(checkUpdate).catch(onError);
break;
case 'command':
// var v = state.val.replace(/^on$|red|green|blue|transition|bri|off|false|#/g, function (match) {
// return {
// 'false': 0,
// '#': '#',
// of: 'off:1',
// on: 'on:1',
// red: 'r',
// green: 'g',
// blue: 'b',
// white: 'w',
// transition: 'x',
// bri: 'l',
// off: 'on:0'
// }[match]
// });
// v = v.replace(/\s|"|;$|,$/g, '').replace(/=/g, ':').replace(/;/g, ',').replace(/true/g, 1).replace(/#((\d|[a-f]|[A-F])*)/g, 'h:"$1"').replace(/(r|g|b|w|x|l|sat|of|on|ct|h)/g, '"$1"').replace(/^\{?(.*?)\}?$/, '{$1}');
var v = state.val.replace(/^on$|red|green|blue|off|false|true|#|=|;/g, function(match) { return { ';':',', '=': ':', 'true': 1, 'false': 0, '#': '#', of:'off:1', on:'on:1', red:'r', green:'g', blue:'b', white: 'w', off:'on:0'}[match] });
//v = v.replace(/h:|\s|\"|;$|,$/g, '').replace(/(#(\d|[a-f]|[A-F])*)/g, 'hex:$1').replace(/([\w|#]+)/g, '"$1"').replace(/^\{?(.*?)\}?$/, '{$1}');
v = v.replace(/h:|\s|\"|;$|,$/g, '').replace(/(#(\d|[a-f]|[A-F])*)/g, 'hex:$1').replace(/(#[a-fA-F0-9]+|[a-zA-Z]+)/g, '"$1"').replace(/^\{?(.*?)\}?$/, '{$1}');
var colors_;
try {
colors_ = JSON.parse(v);
} catch (e) {
adapter.log.error('on Command: ' + e.message + ': state.val="' + state.val + '"');
return;
}
if (colors_.hex !== undefined) {
var co = parseHexColors(colors_.hex);
colors_.r = co.r;
colors_.g = co.g;
colors_.b = co.b;
delete colors_.hex;
}
if (!colors_ || typeof colors_ !== 'object') {
return;
}
var obj = soef.njs.fullExtend(aktStates(), colors_);
adapter.log.debug(JSON.stringify(obj));
if (obj.transition !== undefined) {
transitionTime = obj.transition >> 0;
}
if (colors_.r !== undefined || colors_.g !== undefined || colors_.b !== undefined || colors_.sat !== undefined) {
lightify.nodeColor(mac, obj.r, obj.g, obj.b, obj.sat, transitionTime).catch(onError);
}
if (obj.on !== undefined) {
lightify.nodeOnOff(mac, !!(obj.on >> 0)).catch(onError);
}
if (obj.ct !== undefined) {
lightify.nodeTemperature(mac, obj.ct >> 0, transitionTime).catch(onError);
}
if (obj.bri !== undefined) {
lightify.nodeBrightness(mac, obj.bri >> 0, transitionTime).catch(onError);
}
break;
default:
return
}
var to = transitionTime*100 > 800 ? transitionTime*100 : 800;
refreshTimer.set(updateDevices, to, mac);
}
var tf = {
BRI: 0xAE, // ((~FT_SWITCH) & (~FT_PLUG)) & 0xff, //0xffee, // 0x04?
CT: 0x02,
RGB: 0x08,
SWITCH: 0x40,
PLUG: 0x10,
LIGHT: 0xAE,
ALL: 0xFF
};
var usedStateNames = {
type: {n: 'type', g: 1, tf: tf.ALL, val: 0, common: {read: true, min: 0, max: 255, write: false, type: 'number', role: 'state'}},
online: {n: 'reachable', g: 1, tf: tf.ALL, val: 0, common: {read: true, write: false, type: 'boolean', role: 'indicator.connected'}},
groupid: {n: 'groupid', g: 1, tf: tf.ALL, val: 0, common: {read: true, write: false, type: 'string', role: 'state'}},
status: {n: 'on', g: 7, tf: tf.ALL, val: false, common: {read: true, write: true, type: 'boolean', role: 'switch'}},
//statusSoft: { n: 'softOn', g:7, tf: tf.ALL, val: false, common: { min: false, max: true }},
brightness: {n: 'bri', g: 3, tf: tf.BRI, val: 0, common: {read: true, write: true, min: 0, max: 100, unit: '%', desc: '0..100%', type: 'number', role: 'level.dimmer'}},
temperature: {n: 'ct', g: 1, tf: tf.CT, val: 0, common: {read: true, write: true, min: 2700, max: 6500, unit: '°K', desc: 'in °Kelvin 2700..6500', type: 'number', role: 'level.color.temperature'}},
red: {n: 'r', g: 1, tf: tf.RGB, val: 0, common: {read: true, write: true, min: 0, max: 255, type: 'number', role: 'level.color.red'}},
green: {n: 'g', g: 1, tf: tf.RGB, val: 0, common: {read: true, write: true, min: 0, max: 255, type: 'number', role: 'level.color.green'}},
blue: {n: 'b', g: 1, tf: tf.RGB, val: 0, common: {read: true, write: true, min: 0, max: 255, type: 'number', role: 'level.color.blue'}},
alpha: {n: 'sat', g: 1, tf: tf.RGB, val: 0, common: {read: true, write: true, min: 0, max: 255, type: 'number', role: 'level.color.saturation'}},
transition: {n: 'trans', g: 3, tf: tf.LIGHT, val: 30, common: {read: true, write: true, unit: '\u2152 s', desc: 'in 10th seconds', type: 'number', role: 'state'} },
command: {n: 'command', g: 3, tf: tf.LIGHT, val: 'r:0, g:0, b:0, sat:255, on:true, transition:20', common: {read: true, write: true, type: 'string', role: 'state'}},
refresh: {n: 'refresh', g: 1, tf: tf.LIGHT, val: false, common: {desc: 'read states from device', type: 'boolean', role: 'button'}},
rgb: {n: 'rgb', g: 1, tf: tf.RGB, val: '', common: {desc: '#000000..#ffffff', type: 'string', role: 'level.color.rgb'}}
};
var F_DEVICE = 1;
var F_GROUP = 2;
var F_ALL = 4;
var groupSuffix = '00000000000000';
//var _00000000000000 = '00000000000000';
var groupIdAll = 'FFFFFFFFFFFFFFFF';
function isGroupId(id) {
return lightify.isGroup(id);
// if (!id || id.length < groupSuffix.length + 2) {
// return false;
// }
// return id === groupIdAll || id.substr(2) === groupSuffix;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function createAll(callback) {
var dev = new devices.CDevice(0, '');
var existingDevices = [];
function create(data, gFlag) {
for (var i = 0; i < data.result.length; i++) {
var device = data.result[i];
//if (typeof device.mac !== 'string') device.mac = device.friendlyMac.toUpperCase();
existingDevices.push(device.mac);
//dev.setDevice(device.mac, {common: {name: device.name, role: gFlag&F_DEVICE?'Device':'Group'}, native: { mac: device.mac, groups: device.groupid } });
dev.setChannel(device.mac, {
common: {
name: device.name,
role: gFlag & F_DEVICE ? 'Device' : 'Group'
},
native: {
mac: device.mac,
groups: device.groupid,
id: device.id,
//TODO update group our zone changes
devices: device.devices
}
});
for (var j in usedStateNames) {
if ((usedStateNames[j].g & gFlag && (gFlag & F_GROUP)) || (device.type & usedStateNames[j].tf)) {
var st = Object.assign({}, usedStateNames[j]);
dev.createNew(st.n, st);
}
}
}
}
function checkDeletedDevices() {
devices.foreach('*', function (id) {
if (id.indexOf('.') >= 0 || id === 'refresh'/*|| isGroupId(id)*/) {
return true;
}
if (!existingDevices.find(function (v) {
return v === id;
})) {
//soef.njs.dcs_old(id);
//adapter.deleteChannel(id);
dcs.del(id);
//deleteObjectWithStates(id);
//devices.remove(id);
}
return true;
});
}
lightify.discover().then(function (data) {
create (data, F_DEVICE);
var all = {
mac: groupIdAll,
name: 'All',
devices: []
};
data.result.forEach(function(o) {
all.devices.push(o.mac);
});
create( { result: [ all ] }, F_GROUP);
lightify.discoverZoneEx().then(function (data) {
// for (var i=0; i<data.result.length; i++) {
// data.result[i].mac = soef.sprintf('%02X00000000000000', data.result[i].id);
// data.result[i].friendlyMac = data.result[i].mac;
// }
create(data, F_GROUP);
//devices.update(callback);
checkDeletedDevices();
dev.update(function () {
devices.root.set('refresh', usedStateNames.refresh);
devices.update(callback);
});
}).catch(onError);
}).catch(onError);
}
function updateDevices(mac) {
function update(data) {
var g = 1;
if (!data.result) {
data = {
result: [data]
};
}
var dev = new devices.CDevice(0, '');
for (var i = 0; i < data.result.length; i++) {
var device = data.result[i];
if (device.status !== undefined) {
device.status = !!device.status;
}
if (typeof device.mac !== 'string') continue;
if (device.type) {
types[device.mac] = device.type;
}
dev.setChannel(device.mac); //, device.name ? {common: {name: device.name}} : undefined); //, {common: {name: device.name}});
dev.setName(device.name);
var o = {};
o.bri = device.brightness; // * 10;// * 2560;
o.red = Math.round((device.red * o.bri) / 100);
o.green = Math.round((device.green * o.bri) / 100);
o.blue = Math.round((device.blue * o.bri) / 100);
device.rgb = soef.sprintf('#%02X%02X%02X', o.red, o.green, o.blue, device.alpha);
device.type = device.type || types[device.mac];
for (var j in usedStateNames) {
if (usedStateNames[j].g & g && device[j] !== undefined && (device.type & usedStateNames[j].tf)) {
dev.set(usedStateNames[j].n, device[j]);
}
}
if (usedStateNames.statusSoft !== undefined) dev.set(usedStateNames.statusSoft.n, device.status);
}
dev.update();
}
if (mac && lightify.getStatus !== undefined && !isGroupId(mac)) {
lightify.getStatus(mac).then(update).catch(onError);
} else {
lightify.discover().then(update).catch(onError);
}
}
function poll() {
updateDevices();
if (!adapter.config.polling || adapter.config.interval <= 0) {
return;
}
updateTimer.set(poll, adapter.config.interval * 1000);
}
function browseMdns(callback) {
var Mdns = require('mdns-discovery');
var mdns = new Mdns({
timeout: 3,
returnOnFirstFound: true,
name: '_http._tcp.local',
find: 'Lightify',
broadcast: false
});
mdns.run(callback);
}
function browseIp(callback) {
var ips = getIPAddresses();
var result = [];
if (!ips.length) {
return callback && callback([]);
}
var count = 0;
ips.forEach(function (ownIP) {
var prefixIP = ownIP.split('.', 3).join('.') + '.';
adapter.log.info('Own IP: ' + ownIP + ' Range: ' + prefixIP + '1...255');
for (var i = 0; i < 255; i++) {
count++;
tryIp(prefixIP + i, function (foundIp) {
if (foundIp) {
result.push(foundIp);
}
if (!--count && callback) {
callback(result);
}
});
}
});
}
function getIPAddresses() {
// found on stackOverflow
var ips = [];
var interfaces = require('os').networkInterfaces();
for (var devName in interfaces) {
if (!interfaces.hasOwnProperty(devName)) continue;
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
ips.push(alias.address);
}
}
}
return ips;
}
function tryIp(ip, cb) {
net = net || require('net');
var client = new net.Socket();
client.setTimeout(1000, function () {
try {
client.destroy();
} catch (e) {
}
cb(null);
});
client.on('data', function (data) {
});
client.on('error', function (error) {
try {
client.destroy();
} catch (e) {
}
cb(null);
});
client.on('connect', function () {
try {
client.end();
} catch (e) {
}
cb(ip);
});
client.connect(4000, ip, function () {
});
}
function browse(callback) {
try {
browseMdns(callback);
} catch (e) {
browseIp(callback);
}
}
function checkIP(callback) {
if (adapter.config.ip) {
return callback();
}
adapter.log.info('No IP configured, trying to find a gateway...');
browse(function (list) {
if (list && list.length) {
adapter.log.info('Found IP: ' + list[0].ip);
soef.changeAdapterConfig(adapter, function(config) {
config.ip = list[0].ip;
}, process.exit);
} else {
adapter.log.warn('No IP defined and nothing found');
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function normalizeConfig(config) {
if (config.interval === undefined && config.intervall !== undefined) config.interval = config.intervall;
config.interval = config.interval >> 0; // same as parseInt()
config.polling = !!config.polling;
}
var errorCnt = 0;
var oTimeout;
function onError(error) {
switch (error.errno) { //error.code
//case undefined:
// if (error.message != "This socket is closed") return;
case 'ETIMEDOUT':
case 'ECONNRESET':
case 'EPIPE':
if (oTimeout) clearTimeout(oTimeout);
oTimeout = setTimeout(start, 3000);
setConnectionState(false);
return;
}
if (error === 'timeout') {
if (oTimeout) clearTimeout(oTimeout);
oTimeout = setTimeout(start, 3000);
setConnectionState(false);
}
}
function onConnectError(err) {
if (err === 'connect timeout') {
setTimeout(start, errorCnt <= 5 ? 1000 : 10000);
if (errorCnt++ === 5) {
adapter.log.error('Can not connect to Lightify Gateway "' + adapter.config.ip + '"');
}
setConnectionState(false);
}
}
function start() {
closeAll();
lightify = new Lightify.lightify(adapter.config.ip, adapter.log, onError);
//lightify.setAutoCloseConnection(true);
//isGroupId = lightify.isGroup;
//lightify.connectEx(function() {
lightify.connect(onError).then(function () {
setConnectionState(true);
errorCnt = 0;
createAll(poll);
}).catch(onConnectError);
}
function main() {
normalizeConfig(adapter.config);
setConnectionState(false);
checkIP(function () {
start();
adapter.subscribeStates('*');
});
}
/*
type 2: SurfaceTW, LIGHTIFY Surface Light Turable White
type 10: A60RGBW, LIGHTIFY CLA 60 RGBW
type 4: SurfaceW, LIGHTIFY Surface Light W 28
*/
//https://api.github.com/repos/soef/node-lightify/tarball/master
/*
Error Codes:
0x00: No error?
0x01: Wrong (number of) parameter(s)?
0x14: ?
0x15: Command is not a broadcast?
0x16: ?
0xA7: ?
0x0B: ?
0xC2: ?
0xD1: ?
0xFF: Unknown command?
*/