hm2mqtt
Version:
Interface between Homematic and MQTT
1,085 lines (998 loc) • 35.3 kB
JavaScript
/* eslint-disable prefer-destructuring */
const fs = require('fs');
const path = require('path');
const pjson = require('persist-json')('hm2mqtt');
const async = require('async');
const log = require('yalm');
const request = require('request');
const Mqtt = require('mqtt');
const xmlrpc = require('homematic-xmlrpc');
const binrpc = require('binrpc');
const discover = require('./discover.js');
const pkg = require('./package.json');
const config = require('./config.js');
log.setLevel(config.verbosity);
log.info(pkg.name + ' ' + pkg.version + ' starting');
const rpcClient = {};
const rpcServer = {};
const ifaceConnected = {};
let ifaceAllConnected = false;
const values = {};
const changes = {};
const working = {};
const workingTimeout = {};
let names = {};
const addresses = {};
function reverseNames() {
Object.keys(names).forEach(address => {
addresses[names[address]] = address;
});
}
const devices = {};
log.debug('loading', 'paramsetDescriptions_' + fileName());
const paramsetDescriptions = pjson.load('paramsetDescriptions_' + fileName()) || {};
const getParamsetTimeout = {};
const paramsetQueue = {};
const lastEvent = {};
const programs = {};
const programNames = {};
const variables = {};
const variableNames = {};
const variableType = {
2: 'BOOL',
4: 'FLOAT',
16: 'INTEGER',
20: 'STRING'
};
let mqttConnected;
log.info('mqtt trying to connect', config.mqttUrl);
const mqtt = Mqtt.connect(config.mqttUrl, {
clientId: config.name + '_' + Math.random().toString(16).substr(2, 8),
will: {topic: config.name + '/connected', payload: '0', retain: (config.mqttRetain)},
rejectUnauthorized: !config.insecure
});
mqtt.on('connect', () => {
mqttConnected = true;
log.info('mqtt connected', config.mqttUrl);
mqtt.publish(config.name + '/connected', ifaceAllConnected ? '2' : '1', {retain: (config.mqttRetain)});
log.info('mqtt subscribe', config.name + '/set/#');
mqtt.subscribe(config.name + '/set/#');
log.info('mqtt subscribe', config.name + '/param/#');
mqtt.subscribe(config.name + '/param/#');
log.info('mqtt subscribe', config.name + '/paramset/#');
mqtt.subscribe(config.name + '/paramset/#');
log.info('mqtt subscribe', config.name + '/rega/#');
mqtt.subscribe(config.name + '/rega/#');
log.info('mqtt subscribe', config.name + '/rpc/#');
mqtt.subscribe(config.name + '/rpc/#');
log.info('mqtt subscribe', config.name + '/command/#');
mqtt.subscribe(config.name + '/command/#');
});
mqtt.on('close', () => {
if (mqttConnected) {
mqttConnected = false;
log.error('mqtt closed ' + config.mqttUrl);
}
});
mqtt.on('error', err => {
log.error('mqtt', err);
});
mqtt.on('close', () => {
log.warn('mqtt close');
});
mqtt.on('offline', () => {
log.warn('mqtt offline');
});
mqtt.on('reconnect', () => {
log.info('mqtt reconnect');
});
function mqttPublish(topic, payload, options) {
if (typeof payload === 'object') {
payload = JSON.stringify(payload);
} else if (payload) {
payload = String(payload);
} else {
payload = '';
}
mqtt.publish(topic, payload, options, err => {
if (err) {
log.error('mqtt publish', err);
} else {
log.debug('mqtt >', topic, payload);
}
});
}
mqtt.on('message', (topic, payload) => {
payload = payload.toString();
log.debug('mqtt <', topic, payload);
const parts = topic.split('/');
if (parts.length >= 4 && parts[1] === 'set') {
// Topic <name>/set/<channel>/<datapoint>
const channel = parts.slice(2, parts.length - 1).join('/');
const datapoint = parts[parts.length - 1];
rpcSet(channel, 'VALUES', datapoint, payload);
} else if (parts.length >= 5 && parts[1] === 'param') {
// Topic <name>/param/<channel>/<paramset>/<datapoint>
const channel = parts.slice(2, parts.length - 2).join('/');
const paramset = parts[parts.length - 2];
const datapoint = parts[parts.length - 1];
rpcPutParam(channel, paramset, datapoint, payload);
} else if (parts.length >= 4 && parts[1] === 'paramset') {
// Topic <name>/paramset/<channel>/<paramset>
const channel = parts.slice(2, parts.length - 1).join('/');
const paramset = parts[parts.length - 1];
rpcPutParamset(channel, paramset, payload);
} else if (parts.length === 5 && parts[1] === 'rpc') {
// Topic <name>/rpc/<interface>/<command>/<call_id> - Answer: <name>/response/<call_id>
const [, , iface, command, callid] = parts;
rpc(iface, command, callid, payload);
} else if (parts.length >= 3 && parts[1] === 'rega') {
// Topic <name>/rega/<variableOrProgramName>
const name = parts.slice(2, parts.length);
if (variables[name]) {
setVar(variables[name], payload);
} else if (programs[name]) {
setProgram(programs[name], payload);
} else {
log.error('unknown variable/program', name);
}
} else if (parts[1] === 'command') {
switch (parts[2]) {
case 'regasync':
getRegaDeviceNames();
break;
default:
log.error('mqtt < unknown command', parts[2]);
}
} else {
log.error('mqtt <', topic, payload);
}
});
function setVar(variable, payload) {
let val;
if (payload.indexOf('{') === 0) {
try {
val = JSON.parse(payload).val;
} catch (err) {
val = payload;
}
} else {
val = payload;
}
switch (variable.type) {
case 'BOOL':
// OMG this is so ugly...
if (val === 'false') {
val = false;
} else if (!isNaN(val)) { // Make sure that the string "0" gets casted to boolean false
val = Number(val);
}
val = Boolean(val);
break;
case 'FLOAT':
val = parseFloat(val) || 0;
break;
case 'INTEGER':
if (typeof val === 'string') {
if (variable.enum && (variable.enum.indexOf(val) !== -1)) {
val = variable.enum.indexOf(val);
}
}
val = parseInt(val, 10) || 0;
break;
case 'STRING':
val = '"' + String(val) + '"';
break;
default:
}
const script = 'dom.GetObject(' + variable.id + ').State(' + val + ');';
log.debug('rega >', script);
rega(script, err => {
if (err) {
log.error(err);
}
});
}
function setProgram(program, payload) {
log.debug(program, payload);
let val;
let script;
if (payload.indexOf('{') === 0) {
try {
val = JSON.parse(payload).val;
} catch (err) {
val = payload;
}
} else {
val = payload;
}
if (val === 'start') {
script = 'dom.GetObject(' + program.id + ').ProgramExecute();';
} else {
if (val === 'false') {
val = false;
}
val = Boolean(val);
script = 'dom.GetObject(' + program.id + ').Active(' + val + ');';
}
log.debug('rega >', script);
rega(script, err => {
if (err) {
log.error(err);
}
});
}
function rpc(iface, command, callid, payload) {
if (rpcClient[iface]) {
let params;
if (payload.indexOf('[') === 0) {
try {
params = JSON.parse(payload);
} catch (err) {
log.error(err);
}
} else if (params) {
params = [params];
} else {
params = [];
}
log.debug('rpc', iface, '>', command, params);
rpcClient[iface].methodCall(command, params, (err, res) => {
if (err) {
log.error(err);
} else {
const topic = config.name + '/response/' + callid;
const payload = JSON.stringify(res);
mqttPublish(topic, payload);
}
});
}
}
function rpcType(payload, paramset) {
let val;
if (payload.indexOf('{') === 0) {
try {
val = JSON.parse(payload).val;
} catch (err) {
val = payload;
}
} else {
val = payload;
}
switch (paramset && paramset.TYPE) {
case 'BOOL':
// eslint-disable-line no-fallthrough
case 'ACTION':
// OMG this is so ugly...
if (val === 'false') {
val = false;
} else if (!isNaN(val)) { // Make sure that the string "0" gets casted to boolean false
val = Number(val);
}
val = Boolean(val);
break;
case 'FLOAT':
val = parseFloat(val);
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
}
val = {explicitDouble: val};
break;
case 'ENUM':
if (typeof val === 'string') {
if (paramset.ENUM && (paramset.ENUM.indexOf(val) !== -1)) {
val = paramset.ENUM.indexOf(val);
}
}
// eslint-disable-line no-fallthrough
case 'INTEGER':
val = parseInt(val, 10);
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
}
break;
case 'STRING':
val = String(val);
break;
default:
}
return val;
}
function findIface(address) {
let iface = null;
Object.keys(devices).forEach(i => {
if (devices[i] && devices[i][address]) {
iface = i;
}
});
return iface;
}
function rpcPutParam(name, paramsetKey, datapoint, payload) {
const address = addresses[name] || name;
const iface = findIface(address);
if (!iface) {
log.error('unknown device', address);
return;
}
const psName = paramsetName(devices[iface][address]);
let ps = paramsetDescriptions[psName];
ps = ps && ps[paramsetKey] && ps[paramsetKey][datapoint];
if (!ps) {
log.warn('unknown paramset', paramsetName(devices[iface][address]) + '.' + paramsetKey + '.' + datapoint);
}
if (ps && !(ps.OPERATIONS & 2)) {
log.error(iface, address, paramsetKey, datapoint, 'not writeable');
return;
}
const val = rpcType(payload, ps);
const paramset = {};
paramset[datapoint] = val;
log.debug('rpc', iface, '> putParamset', [address, paramsetKey, paramset]);
rpcClient[iface].methodCall('putParamset', [address, paramsetKey, paramset], err => {
if (err) {
log.error(err);
}
});
}
function rpcPutParamset(name, paramsetKey, payload) {
const address = addresses[name] || name;
const iface = findIface(address);
if (!iface) {
log.error('unknown device', address);
return;
}
const psName = paramsetName(devices[iface][address]);
let ps = paramsetDescriptions[psName];
ps = ps && ps[paramsetKey];
if (!ps) {
log.warn('unknown paramset', paramsetName(devices[iface][address]) + '.' + paramsetKey);
}
const paramset = {};
try {
payload = JSON.parse(payload);
if (typeof payload !== 'object') {
throw new TypeError('invalid payload type', typeof payload);
}
Object.keys(payload).forEach(datapoint => {
if (ps[datapoint] && !(ps[datapoint].OPERATIONS & 2)) {
log.error(iface, address, paramsetKey, datapoint, 'not writeable');
return;
}
paramset[datapoint] = rpcType(String(payload[datapoint]), ps[datapoint]);
});
} catch (err) {
log.error(err);
return;
}
log.debug('rpc', iface, '> putParamset', [address, paramsetKey, paramset]);
rpcClient[iface].methodCall('putParamset', [address, paramsetKey, paramset], err => {
if (err) {
log.error(err);
}
});
}
function rpcSet(name, paramset, datapoint, payload) {
const address = addresses[name] || name;
const iface = findIface(address);
if (!iface) {
log.error('unknown device', address);
return;
}
const psName = paramsetName(devices[iface][address]);
let ps = paramsetDescriptions[psName];
ps = ps && ps[paramset] && ps[paramset][datapoint];
if (!ps) {
log.warn('unknown paramset', paramsetName(devices[iface][address]) + '.' + paramset + '.' + datapoint);
}
if (ps && !(ps.OPERATIONS & 2)) {
log.error(iface, address, paramset, datapoint, 'not writeable');
return;
}
const val = rpcType(payload, ps);
log.debug('rpc', iface, '> setValue', [address, datapoint, val]);
rpcClient[iface].methodCall('setValue', [address, datapoint, val], err => {
if (err) {
log.error(err);
}
});
}
function rega(script, callback) {
const url = 'http://' + config.ccuAddress + ':8181/rega.exe';
log.debug('sending script to', url);
request({
method: 'POST',
url,
body: script,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': script.length
}
}, (err, res, body) => {
if (!err && body) {
const end = body.indexOf('<xml>');
const data = body.substr(0, end);
callback(null, data);
} else {
callback(err);
}
});
}
function regaJson(file, callback) {
const filepath = path.join(__dirname, 'regascripts', file);
const script = fs.readFileSync(filepath).toString();
rega(script, (err, res) => {
if (err) {
log.error(err);
} else {
try {
callback(null, JSON.parse(unescape(res)));
} catch (err) {
callback(err);
}
}
});
}
function getRegaDeviceNames(cb) {
regaJson('devices.fn', (err, res) => {
if (err) {
log.error(err);
} else {
names = res;
reverseNames();
log.info('got', Object.keys(names).length, 'names from rega. saving', 'names_' + fileName());
pjson.save('names_' + fileName(), names);
if (typeof cb === 'function') {
cb();
}
}
});
}
if (config.jsonNameTable) {
log.info('loading name table', config.jsonNameTable);
names = require(config.jsonNameTable);
reverseNames();
} else if (!config.disableRega) {
log.info('loading', 'names_' + fileName());
names = pjson.load('names_' + fileName()) || {};
getRegaDeviceNames(() => {
if (config.regaPollInterval || config.regaPollTrigger) {
getPrograms(() => {
getVariables();
});
}
if (config.regaPollInterval > 0) {
log.debug('rega poll interval', config.regaPollInterval);
setInterval(() => {
getPrograms(() => {
getVariables();
});
}, config.regaPollInterval * 1000);
}
});
}
function parseDateISOString(s) {
const ds = s.split(/\D/).map(s => parseInt(s, 10));
ds[1] -= 1;
return new Date(...ds);
}
function getVariables() {
regaJson('variables.fn', (err, res) => {
if (err) {
log.error(err);
} else {
Object.keys(res).forEach(id => {
const varName = res[id].name;
let change = false;
if (!variables[varName] || (res[id].val !== variables[varName].val) || (res[id].ts !== variables[varName].ts)) {
change = true;
}
variables[varName] = {
id: Number(id),
val: res[id].val,
min: res[id].min,
max: res[id].max,
unit: res[id].unit,
ts: res[id].ts,
type: variableType[res[id].type],
enum: res[id].enum ? res[id].enum.split(';') : undefined
};
variableNames[Number(id)] = varName;
if (change) {
const ts = res[id].ts ? parseDateISOString(res[id].ts).getTime() : 0;
const topic = config.name + '/status/' + varName;
let enumIndex = res[id].val;
if (enumIndex === false) {
enumIndex = 0;
} else if (enumIndex === true) {
enumIndex = 1;
}
let payload = {
val: res[id].val,
ts,
hm: {
id: Number(id),
UNIT: res[id].unit,
MIN: res[id].min,
MAX: res[id].max,
ENUM: res[id].enum ? res[id].enum.split(';')[enumIndex] : undefined
}
};
payload = JSON.stringify(payload);
mqttPublish(topic, payload, {retain: (config.mqttRetain)});
}
});
log.debug('rega got', Object.keys(variables).length, 'variables');
}
});
}
function getPrograms(cb) {
regaJson('programs.fn', (err, res) => {
if (err) {
log.error(err);
cb(err);
} else {
Object.keys(res).forEach(id => {
const programName = res[id].name;
let change = false;
if (!programs[programName] || (res[id].val !== programs[programName].val) || (res[id].ts !== programs[programName].ts)) {
change = true;
}
programs[programName] = {
id: Number(id),
active: res[id].active,
ts: res[id].ts
};
programNames[Number(id)] = programName;
if (change) {
const ts = res[id].ts ? parseDateISOString(res[id].ts).getTime() : 0;
const topic = config.name + '/status/' + programName;
let payload = {
val: res[id].active,
ts,
hm: {
id: Number(id)
}
};
payload = JSON.stringify(payload);
mqttPublish(topic, payload, {retain: (config.mqttRetain)});
}
});
log.debug('rega got', Object.keys(programs).length, 'programs');
cb(null);
}
});
}
log.debug('discover interfaces');
discover(config.ccuAddress, {
// Todo... cuxd: {port: 8701, protocol: 'binrpc'},
rfd: {port: 2001, protocol: 'binrpc'},
hs485d: {port: 2000, protocol: 'binrpc'},
hmip: {port: 2010, protocol: 'xmlrpc'}
}, interfaces => {
Object.keys(interfaces).forEach(iface => {
createIface(iface, interfaces[iface].protocol, interfaces[iface].port);
if (iface === 'hmip' && config.hmipReconnectInterval) {
setInterval(() => {
checkInit(iface, interfaces[iface].protocol);
}, config.hmipReconnectInterval * 1000);
} else if (iface === 'cuxd') {
// TODO
} else if (config.pingInterval) {
setInterval(() => {
checkInit(iface, interfaces[iface].protocol);
}, config.pingInterval * 1000);
}
});
});
function fileName(name) {
return config.ccuAddress + (name ? '_' + name : '');
}
function checkInit(iface, protocol) {
const now = (new Date()).getTime();
const elapsed = Math.ceil((now - (lastEvent[iface] || 0)) / 1000);
log.debug(iface, 'elapsed since lastevent:', elapsed + 's');
if (iface === 'hmip' && config.hmipReconnectInterval) {
if (elapsed >= config.hmipReconnectInterval) {
ifaceConnected[iface] = false;
initIface(iface, protocol);
}
} else if (iface === 'cuxd') {
// TODO cuxd reconnect? ping possible?
} else if (elapsed >= (config.pingInterval * 2)) {
ifaceConnected[iface] = false;
initIface(iface, protocol);
} else if (elapsed >= config.pingInterval) {
log.debug('rpc', iface, '> ping');
rpcClient[iface].methodCall('ping', ['hm2mqtt'], err => {
if (err) {
log.error(err);
}
});
}
checkIfaceAll();
}
function checkIfaceAll() {
const current = ifaceAllConnected;
ifaceAllConnected = true;
Object.keys(ifaceConnected).forEach(i => {
if (!ifaceConnected[i]) {
ifaceAllConnected = false;
}
});
log.debug('ifaceAllConnected', ifaceAllConnected);
if (current !== ifaceAllConnected) {
mqtt.publish(config.name + '/connected', ifaceAllConnected ? '2' : '1', {retain: (config.mqttRetain)});
}
}
function createIface(name, protocol, port) {
log.debug('loading', 'devices_' + fileName(name));
devices[name] = pjson.load('devices_' + fileName(name));
log.debug('createIface', name, protocol, port);
if (!rpcServer[protocol]) {
rpcServer[protocol] = createServer(protocol);
}
rpcClient[name] = createClient(protocol, port);
initIface(name, protocol, port);
if (config.dutyCyclePollInterval && (name === 'hmip' || name === 'rfd')) {
setInterval(() => {
pollDutyCylce(name);
}, config.dutyCyclePollInterval * 1000);
}
}
const stopIface = {};
function stop() {
const cmdQueue = [];
Object.keys(stopIface).forEach(iface => {
cmdQueue.push(stopIface[iface]);
});
async.parallel(cmdQueue, () => {
process.exit(0);
});
setTimeout(() => {
process.exit(1);
}, 2500);
}
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
function initIface(name, protocol) {
let url;
if (protocol === 'binrpc') {
url = 'xmlrpc_bin://' + (config.initAddress || config.listenAddress) + ':' + config.binrpcListenPort;
} else {
url = 'http://' + (config.initAddress || config.listenAddress) + ':' + config.listenPort;
}
const params = [url, 'hm2mqtt_' + name];
log.info('rpc', name, '> init', params);
lastEvent[name] = (new Date()).getTime();
rpcClient[name].methodCall('init', params, (err, res) => {
if (err) {
log.error(err);
} else {
log.debug('rpc', name, '< init', JSON.stringify(res));
ifaceConnected[name] = true;
checkIfaceAll();
}
stopIface[name] = cb => {
const stopParams = [url, ''];
log.info('rpc', name, '> init', stopParams);
rpcClient[name].methodCall('init', stopParams, (err, res) => {
log.debug('rpc', name, '< init', err, JSON.stringify(res));
ifaceConnected[name] = false;
checkIfaceAll();
cb();
});
};
});
}
function createClient(protocol, port) {
let client;
const options = {
host: config.ccuAddress,
port,
path: '/'
};
if (protocol === 'binrpc') {
client = binrpc.createClient(options);
} else {
client = xmlrpc.createClient(options);
}
return client;
}
function paramsetName(dev) {
return dev.PARENT_TYPE + '/' + dev.VERSION + '/' + dev.TYPE;
}
function publishMeta(name) {
log.info('publish meta data', name);
Object.keys(devices[name]).forEach(address => {
const dev = devices[name][address];
const psDesc = paramsetDescriptions[paramsetName(dev)];
const obj = {
name: names[address] || address,
type: dev.PARENT_TYPE ? 'channel' : 'device',
interface: 'homematic',
native: dev
};
obj.native.PARAMSET_DESCRIPTIONS = psDesc;
mqttPublish('db/extend/' + config.name + '/' + address, obj);
});
}
function createParamsetQueue(name) {
name = ifaceName(name);
if (!devices[name]) {
log.error('createParamsetQueue called for unknown devices', name);
return;
}
log.info(name, 'got', Object.keys(devices[name]).length, 'devices and channels');
log.debug('createParamsetQueue', name);
if (!paramsetQueue[name]) {
paramsetQueue[name] = [];
}
Object.keys(devices[name]).forEach(address => {
const dev = devices[name][address];
if (!dev.PARENT_TYPE) {
return;
}
if (!paramsetDescriptions[paramsetName(dev)]) {
log.debug('unknown', paramsetName(dev), dev.PARAMSETS);
paramsetDescriptions[paramsetName(dev)] = {};
dev.PARAMSETS.forEach(ps => {
paramsetQueue[name].push({ADDRESS: dev.ADDRESS, PARAMSET: ps, name: paramsetName(dev)});
});
}
});
getParamset(name);
}
function getParamset(name) {
if (paramsetQueue[name].length > 0) {
const obj = paramsetQueue[name].shift();
log.debug('rpc', name, '> getParamsetDescription', [obj.ADDRESS, obj.PARAMSET], obj.name);
rpcClient[name].methodCall('getParamsetDescription', [obj.ADDRESS, obj.PARAMSET], (err, res) => {
if (!err) {
paramsetDescriptions[obj.name][obj.PARAMSET] = res;
}
setTimeout(() => {
getParamset(name);
}, 500);
});
} else {
log.debug('getParamsetDescriptions', name, 'done');
log.info('got', Object.keys(paramsetDescriptions).length, 'paramsetDescriptions');
log.debug('saving', 'paramsetDescriptions_' + fileName());
pjson.save('paramsetDescriptions_' + fileName(), paramsetDescriptions);
if (config.publishMetadata) {
publishMeta(name);
}
}
}
function ifaceName(id) {
return id.replace(/^hm2mqtt_/, '');
}
const rpcMethods = {
notFound: method => {
log.debug('rpc < Method ' + method + ' does not exist');
},
'system.multicall': (err, params, callback) => {
if (err) {
log.error(err);
return;
}
const res = [];
params[0].forEach(c => {
if (rpcMethods[c.methodName]) {
rpcMethods[c.methodName](err, c.params);
} else {
rpcMethods.notFound(c.methodName, c.params);
}
res.push('');
});
callback(null, res);
},
'system.listMethods': (err, params, callback) => {
if (err) {
log.error(err);
return;
}
log.debug('rpc < system.listMethods', params);
callback(null, Object.keys(rpcMethods));
},
event: (err, params, callback) => {
if (err) {
log.error(err);
return;
}
log.debug('rpc < event', JSON.stringify(params));
const ts = (new Date()).getTime();
lastEvent[ifaceName(params[0])] = ts;
if (params[1] === 'CENTRAL' && params[2] === 'PONG') {
if (typeof callback === 'function') {
callback(null, '');
}
return;
}
if (params[2] === 'WORKING' || params[2] === 'DIRECTION') {
working[params[1]] = Boolean(params[3]);
}
if (config.regaPollTrigger) {
const [regaPollTriggerChannel, regaPollTriggerDatapoint] = config.regaPollTrigger.split('.');
if (params[1] === regaPollTriggerChannel && params[2] === regaPollTriggerDatapoint) {
getPrograms(() => {
getVariables();
});
}
}
const key = params[1] + '/' + params[2];
if (values[key] !== params[3]) {
changes[key] = ts;
values[key] = params[3];
}
const dev = devices[ifaceName(params[0])] && devices[ifaceName(params[0])][params[1]];
if (!dev) {
log.error('unknown device', params[0], params[1]);
return;
}
let ps = paramsetDescriptions[paramsetName(dev)];
if (!ps) {
log.error('unknown paramsetDescription', paramsetName(dev));
} else if (!ps.VALUES) {
log.error('missing VALUES in paramsetDescription', paramsetName(dev));
} else if (!ps.VALUES[params[2]]) {
log.error('missing VALUE', params[2], 'in paramsetDescription', paramsetName(dev));
}
ps = (ps && ps.VALUES && ps.VALUES[params[2]]) || {};
const topic = config.name + '/status/' + (names[params[1]] || params[1]) + '/' + params[2];
let payload = {val: params[3], ts, lc: changes[key], hm: {ADDRESS: params[1]}};
if (ps.UNIT && ps.UNIT !== '""') {
if (ps.UNIT === '�C') {
payload.hm.UNIT = '°C';
} else {
payload.hm.UNIT = ps.UNIT;
}
}
if (ps.TYPE === 'ENUM') {
payload.hm.ENUM = ps.VALUE_LIST[params[3]];
}
payload = JSON.stringify(payload);
const retain = (config.mqttRetain) && (ps.TYPE !== 'ACTION');
mqttPublish(topic, payload, {retain});
if (typeof working[params[1]] !== 'undefined' && (params[2] === 'LEVEL' || params[2] === 'STATE')) {
clearTimeout(workingTimeout[params[1]]);
workingTimeout[params[1]] = setTimeout(() => {
if (!working[params[1]]) {
mqttPublish(topic + '_NOTWORKING', payload, {retain});
}
}, 500);
}
if (typeof callback === 'function') {
callback(null, '');
}
},
listDevices: (err, params, callback) => {
if (err) {
log.error(err);
return;
}
log.debug('rpc < listDevices', params);
const name = ifaceName(params[0]);
const ret = [];
const test = [];
if (devices[name]) {
Object.keys(devices[name]).forEach(address => {
test.push(devices[name][address]);
/* Todo This does not work: https://github.com/eq-3/occu/issues/45
if (name === 'hmip') {
ret.push({
ADDRESS: address,
VERSION: devices[name][address].VERSION,
AES_ACTIVE: devices[name][address].AES_ACTIVE,
CHILDREN: devices[name][address].CHILDREN,
DIRECTION: devices[name][address].DIRECTION,
FIRMWARE: devices[name][address].FIRMWARE,
FLAGS: devices[name][address].FLAGS,
GROUP: devices[name][address].GROUP,
INDEX: devices[name][address].INDEX,
INTERFACE: devices[name][address].INTERFACE,
LINK_SOURCE_ROLES: devices[name][address].LINK_SOURCE_ROLES,
LINK_TARGET_ROLES: devices[name][address].LINK_TARGET_ROLES,
PARAMSETS: devices[name][address].PARAMSETS,
PARENT: devices[name][address].PARENT,
PARENT_TYPE: devices[name][address].PARENT_TYPE,
RF_ADDRESS: devices[name][address].RF_ADDRESS,
ROAMING: devices[name][address].ROAMING,
RX_MODE: devices[name][address].RX_MODE,
TEAM: devices[name][address].TEAM,
TEAM_CHANNELS: devices[name][address].TEAM_CHANNELS,
TEAM_TAG: devices[name][address].TEAM_TAG,
TYPE: devices[name][address].TYPE
});
} else {
ret.push({
ADDRESS: address,
VERSION: devices[name][address].VERSION
});
}
*/
ret.push({
ADDRESS: address,
VERSION: devices[name][address].VERSION
});
});
}
log.debug('>', ret.length);
callback(null, ret);
getParamsetTimeout[params[0]] = setTimeout(() => {
createParamsetQueue(params[0]);
}, 5000);
},
deleteDevices: (err, params, callback) => {
if (err) {
log.error(err);
return;
}
clearTimeout(getParamsetTimeout[params[0]]);
log.debug('rpc < deleteDevices', params[1].length);
const name = ifaceName(params[0]);
params[1].forEach(dev => {
delete devices[name][dev];
});
log.debug('saving', 'devices_' + fileName(name), '(' + Object.keys(devices[name]).length + ')');
pjson.save('devices_' + fileName(name), devices[name]);
callback(null, '');
getParamsetTimeout[params[0]] = setTimeout(() => {
createParamsetQueue(params[0]);
}, 5000);
},
newDevices: (err, params, callback) => {
if (err) {
log.error(err);
return;
}
clearTimeout(getParamsetTimeout[params[0]]);
log.debug('rpc < newDevices', params[1].length);
const name = ifaceName(params[0]);
const devs = devices[name] || {};
params[1].forEach(dev => {
devs[dev.ADDRESS] = dev;
});
devices[name] = devs;
log.debug('saving', 'devices_' + fileName(name), '(' + Object.keys(devices[name]).length + ')');
pjson.save('devices_' + fileName(name), devs);
callback(null, '');
getParamsetTimeout[params[0]] = setTimeout(() => {
createParamsetQueue(params[0]);
}, 5000);
}
};
rpcMethods.NotFound = rpcMethods.notFound;
function createServer(protocol) {
let server;
if (protocol === 'binrpc') {
server = binrpc.createServer({host: config.listenAddress, port: config.binrpcListenPort});
} else {
server = xmlrpc.createServer({host: config.listenAddress, port: config.listenPort});
}
Object.keys(rpcMethods).forEach(method => {
server.on(method, rpcMethods[method]);
});
return server;
}
function pollDutyCylce(iface) {
rpcClient[iface].methodCall('listBidcosInterfaces', [], (err, res) => {
if (err) {
log.error(err);
} else if (res && res.forEach) {
res.forEach(data => {
const topic = config.name + '/status/' + data.ADDRESS + '/DUTY_CYCLE';
const payload = {
val: data.DUTY_CYCLE,
ts: (new Date()).getTime()
};
mqttPublish(topic, payload, {retain: (config.mqttRetain)});
});
}
});
}