UNPKG

iobroker.mysensors

Version:
657 lines (588 loc) 29.1 kB
/* 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 Parses = require('sensors'); const MySensors = require('./lib/mysensors'); const getMeta = require('./lib/getmeta').getMetaInfo; const getMeta2 = require('./lib/getmeta').getMetaInfo2; const adapterName = require('./package.json').name.split('.').pop(); const floatRegEx = /^[+-]?\d+(\.\d*)$/; const config = {}; let serialport; let adapter; let devices = {}; let inclusionOn = false; let inclusionTimeout = false; let presentationDone = false; let path; let fs; let mySensorsInterface; try { serialport = require('serialport'); // .SerialPort; } catch (e) { console.warn('Serial port is not available'); } function startAdapter(options) { options = options || {}; Object.assign(options, {name: adapterName}); adapter = new utils.Adapter(options); // принимаем и обрабатываем сообщения adapter.on('message', obj => { if (obj) { switch (obj.command) { case 'listUart': if (obj.callback) { if (serialport) { // read all found serial ports serialport.list((err, ports) => { adapter.log.info(`List of port: ${JSON.stringify(ports)}`); listSerial(ports); adapter.sendTo(obj.from, obj.command, ports, obj.callback); }); } else { adapter.log.warn('Module serialport is not available'); adapter.sendTo(obj.from, obj.command, [{comName: 'Not available'}], obj.callback); } } break; } } }); // is called when adapter shuts down - callback has to be called under any circumstances! adapter.on('unload', callback => { adapter.setState('info.connection', false, true); try { mySensorsInterface && mySensorsInterface.destroy(); mySensorsInterface = null; callback(); } catch (e) { callback(); } }); // is called if a subscribed state changes adapter.on('stateChange', (id, state) => { if (!state || state.ack || !mySensorsInterface) { return; } // Warning, state can be null if it was deleted adapter.log.debug('stateChange ' + id + ' ' + JSON.stringify(state)); if (id === adapter.namespace + '.inclusionOn') { setInclusionState(state.val); setTimeout(val => adapter.setState('inclusionOn', val, true), 200, state.val); } else // output to mysensors if (devices[id] && devices[id].type === 'state') { const arr_id = id.split('.'); adapter.getState(arr_id[2] + '.' + arr_id[3] + '.255_ARDUINO_NODE.I_PRE_SLEEP_NOTIFICATION', (err, state1) => { if (err) adapter.log.error(err); if (state1 && state1.val && typeof state1.val === 'number' && state1.val > 0) { // determined that node can sleep adapter.getState(arr_id[2] + '.' + arr_id[3] + '.255_ARDUINO_NODE.I_HEARTBEAT_RESPONSE', (err, state2) => { if (err) adapter.log.error(err); if (state2 && state2.val) { // received hearbeat if (Date.now() - state2.lc < state1.val){ adapter.log.debug('Node not sleepping. Send data'); sendMessage(id, state); // node is still awake, sending data } else { adapter.log.debug('Node sleepping. Not send data'); } } }); } else { adapter.log.debug('Node real time.'); sendMessage(id, state); } }); } }); adapter.on('objectChange', (id, obj) => { if (!obj) { if (devices[id]) { delete devices[id]; } } else { if (obj.native.id !== undefined && obj.native.childId !== undefined && obj.native.subType !== undefined) { devices[id] = obj; } } }); adapter.on('ready', () => main()); return adapter; } function filterSerialPorts(path) { // get only serial port names if (!(/(tty(S|ACM|USB|AMA|MFD)|rfcomm)/).test(path)) return false; return fs .statSync(path) .isCharacterDevice(); } function listSerial(ports) { ports = ports || []; path = path || require('path'); fs = fs || require('fs'); // Filter out the devices that aren't serial ports const devDirName = '/dev'; let result; try { result = fs .readdirSync(devDirName) .map(file => path.join(devDirName, file)) .filter(filterSerialPorts) .map(port => { let found = false; for (let v = 0; v < ports.length; v++) { if (ports[v].comName === port) { found = true; break; } } if (!found) { ports.push({comName: port}); } return {comName: port}; }); } catch (e) { if (require('os').platform() !== 'win32') { adapter.log.error(`Cannot read "${devDirName}": ${e}`); } result = []; } return result; } function sendMessage(obj_id, state){ if (typeof state.val === 'boolean') state.val = state.val ? 1 : 0; if (state.val === 'true') { state.val = 1; } if (state.val === 'false') { state.val = 0; } mySensorsInterface.write( devices[obj_id].native.id + ';' + //JG: Changed. Always request an ack when sending command 'set' to a node //devices[id].native.childId + ';1;0;' + devices[obj_id].native.childId + ';1;1;' + devices[obj_id].native.varTypeNum + ';' + state.val, devices[obj_id].native.ip); } function findObjAckFalse(ip, node_id) { for (let id in devices) { if (devices[id].native && (!ip || ip === devices[id].native.ip) && devices[id].native.id == node_id && devices[id].native.childId !== 255 && devices[id].common.write === true) { adapter.getState(id, (err, state) => { if (err) adapter.log.error(err); if (state && state.val && state.ack === false) { adapter.log.debug(`Obj.ack = false. Send state (${id}) to node ${node_id}`); sendMessage(id, state); } }); } } } function setInclusionState(val) { val = val === 'true' || val === true || val === 1 || val === '1'; inclusionOn = val; if (inclusionTimeout) { clearTimeout(inclusionTimeout); } inclusionTimeout = null; if (inclusionOn) { presentationDone = false; } if (inclusionOn && adapter.config.inclusionTimeout) { inclusionTimeout = setTimeout(() => { inclusionOn = false; adapter.setState('inclusionOn', false, true); }, adapter.config.inclusionTimeout); } } function findDevice(result, ip, subType) { for (const id in devices) { if (devices[id].native && (!ip || ip === devices[id].native.ip) && devices[id].native.id == result.id && devices[id].native.childId == result.childId && (subType === false || devices[id].native.varType == result.subType)) { return id; } } return -1; } function saveResult(id, result, ip, subType) { if (id === -1) { id = findDevice(result, ip, subType); } if (id !== -1 && devices[id]) { if (devices[id].common.type === 'boolean') { result.payload = result.payload === 'true' || result.payload === true || result.payload === '1' || result.payload === 1; //result.payload = !!result[i].payload; } if (devices[id].common.type === 'number') { result.payload = parseFloat(result.payload); } adapter.log.debug(`Set value ${devices[id].common.name || id} ${result.childId}: ${result.payload} ${typeof result.payload}`); adapter.setState(id, result.payload, true); return id; } else { return 0; } } function reqGetSend(id, result, ip, subType) { if (id === -1) { id = findDevice(result, ip, subType); } if (id !== -1 && devices[id]) { adapter.getState(id, (err, state) => { err && adapter.log.error(err); if (state && state.val) { try { if (typeof state.val === 'boolean') state.val = state.val ? 1 : 0; if (state.val === 'true') state.val = 1; if (state.val === 'false') state.val = 0; adapter.log.debug(`Get value ${result.id} ${result.childId}: ${state.val}`); mySensorsInterface.write( result.id + ';' + //result.childId + ';1;0;' + // Changed. Always request an ack when sending command 'REQ' to a node result.childId + ';1;1;' + devices[id].native.varTypeNum + ';' + state.val, devices[id].native.ip); } catch (err) { err && adapter.log.error(err); adapter.log.error('Cannot sending!'); } } }); return id; } else { return 0; } } function processPresentation(data, ip, port) { data = data.toString(); let result; try { result = Parses.parse(data); } catch (e) { adapter.log.error('Cannot parse data: ' + data + '[' + e + ']'); return null; } if (!result || !result.length) { adapter.log.warn('Cannot parse data: ' + data); return null; } for (let i = 0; i < result.length; i++) { adapter.log.debug('Got: ' + JSON.stringify(result[i])); if (result[i].type === 'presentation' && result[i].subType) { adapter.log.debug('Message presentation'); presentationDone = true; // Add new node if (inclusionOn) { const found = findDevice(result[i], ip) !== -1; if (!found) { adapter.log.debug('ID not found. Try to add to to DB'); const objs = getMeta(result[i], ip, port, config[ip || 'serial']); for (let j = 0; j < objs.length; j++) { adapter.log.debug(`Check ${JSON.stringify(devices[adapter.namespace + '.' + objs[j]._id])}`); if (!devices[adapter.namespace + '.' + objs[j]._id]) { devices[adapter.namespace + '.' + objs[j]._id] = objs[j]; adapter.log.info(`Add new object: ${objs[j]._id} - ${objs[j].common.name}`); adapter.setObject(objs[j]._id, objs[j], err => err && adapter.log.error(err)); } } } } else { adapter.log.warn('ID not found. Inclusion mode OFF: ' + JSON.stringify(result[i])); } // check if received object exists } else if (result[i].type === 'set' && result[i].subType) { if (0) { adapter.log.debug('Message type is "set". Try to find it in DB...'); let found = false; let foundObjID; // store here ID that suit with parameters to id and childId for (const id in devices) { if ((!ip || ip === devices[id].native.ip) && devices[id].native.id == result[i].id && devices[id].native.childId == result[i].childId && devices[id].native.varType == result[i].subType) { found = true; adapter.log.debug('Found id = ' + id); break; } if (devices[id].native.id == result[i].id && devices[id].native.childId == result[i].childId){ foundObjID = id; adapter.log.debug('Save foundObjID with similar id and childId'); adapter.log.debug('devices[foundObjID].native.id = ' + devices[foundObjID].native.id); adapter.log.debug('devices[foundObjID].native.childId = ' + devices[foundObjID].native.childId); } } // add new value to existing object if (!found && foundObjID) { adapter.log.debug(`Object ID: ${result[i].id}, childId: ${result[i].childId}, subType: ${result[i].subType} not found!`); if (inclusionOn) { adapter.log.debug('ID not found. Try to add to to DB'); const common_name = devices[foundObjID].common.name.split('.'); const objs = getMeta2(result[i], ip, port, config[ip || 'serial'], devices[foundObjID].native.subType, common_name[0]); if (!devices[adapter.namespace + '.' + objs[0]._id]) { devices[adapter.namespace + '.' + objs[0]._id] = objs[0]; adapter.log.info('Add new object: ' + objs[0]._id + ' - ' + objs[0].common.name); adapter.setObject(objs[0]._id, objs[0], err => err && adapter.log.error(err)); } } else { adapter.log.warn('ID ignored by presentation, because inclusion mode OFF: ' + JSON.stringify(result[i])); } } else { if (!found && !foundObjID) { adapter.log.debug(`Object ID: ${result[i].id}, childId: ${result[i].childId} not found!`); } } } // try to convert value let val = result[i].payload; if (floatRegEx.test(val)) { val = parseFloat(val); } if (val === 'true') val = true; if (val === 'false') val = false; result[i].payload = val; } else { // try to convert value let _val = result[i].payload; if (floatRegEx.test(_val)) { _val = parseFloat(_val); } if (_val === 'true') _val = true; if (_val === 'false') _val = false; result[i].payload = _val; } } return result; } function updateSketchName(id, name) { adapter.getObject(id, (err, obj) => { if (!obj) { obj = { type: 'device', common: {name} }; } else if (obj.common.name === name) { name = null; return; } obj.common.name = name; adapter.setObject(adapter.namespace + '.' + id, obj, err => {}); }); } function main() { console.log('1 OPEN SOCKET!'); adapter.getState('info.connection', (err, state) => { if (!state || state.val) { adapter.setState('info.connection', false, true); } }); adapter.config.inclusionTimeout = parseInt(adapter.config.inclusionTimeout, 10) || 0; adapter.getState('inclusionOn', (err, state) => setInclusionState(state ? state.val : false)); // read current existing objects (прочитать текущие существующие объекты) adapter.getForeignObjects(adapter.namespace + '.*', 'state', (err, states) => { // subscribe on changes adapter.subscribeStates('*'); adapter.subscribeObjects('*'); devices = states; if (!devices[adapter.namespace + '.info.connection'] || !devices[adapter.namespace + '.info.connection'].common || (devices[adapter.namespace + '.info.connection'].common.type === 'boolean' && adapter.config.type !== 'serial') || (devices[adapter.namespace + '.info.connection'].common.type !== 'boolean' && adapter.config.type === 'serial')) { adapter.setForeignObject(adapter.namespace + '.info.connection', { _id: 'info.connection', type: 'state', common: { role: 'indicator.connected', name: adapter.config.type === 'serial' ? 'If connected to my sensors' : 'List of connected gateways', type: adapter.config.type === 'serial' ? 'boolean' : 'string', read: true, write: false, def: false }, native: { } }, err => err && adapter.log.error(err)); } mySensorsInterface = new MySensors(adapter.config, adapter.log, error => { // if object created mySensorsInterface.write('0;0;3;0;14;Gateway startup complete'); // process received data mySensorsInterface.on('data', (data, ip, port) => { const result = processPresentation(data, ip, port); // update configuration if presentation received if (!result) { return; } for (let i = 0; i < result.length; i++) { adapter.log.debug('Message type: ' + result[i].type); if (result[i].subType.indexOf('S_') === 0) { adapter.log.debug('Value type of S_... Out of the loop'); return; } const id = findDevice(result[i], ip); if (result[i].type === 'set') { // If set quality if (result[i].subType == 77) { adapter.log.debug('subType = 77'); for (const id in devices) { if (devices[id].native && (!ip || ip === devices[id].native.ip) && devices[id].native.id == result[i].id && devices[id].native.childId == result[i].childId) { adapter.log.debug(`Set quality of ${devices[id].common.name || id} ${result[i].childId}: ${result[i].payload} ${typeof result[i].payload}`); adapter.setState(id, {q: result[i].payload ? 0x40 : 0}, true); } } } else { if (result[i].subType === 'V_LIGHT') { result[i].subType = 'V_STATUS'; } if (result[i].subType === 'V_DIMMER') { result[i].subType = 'V_PERCENTAGE'; } if (result[i].subType === 'V_DUST_LEVEL') { result[i].subType = 'V_LEVEL'; } saveResult(id, result[i], ip, true); } } else if (result[i].type === 'req') { reqGetSend(id, result[i], ip, true); } else if (result[i].type === 'internal') { let saveValue = false; switch (result[i].subType) { case 'I_PRE_SLEEP_NOTIFICATION': // 32 Message sent before node is going to sleep adapter.log.info(`Timeout pre sleep ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); saveValue = true; break; case 'I_POST_SLEEP_NOTIFICATION': // 33 Message sent after node woke up (if enabled) adapter.log.info(`Timeout post sleep ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); saveValue = true; break; case 'I_HEARTBEAT_RESPONSE': // 22 Heartbeat response adapter.log.info(`Heartbeat ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); saveValue = true; break; case 'I_BATTERY_LEVEL': // 0 Use this to report the battery level (in percent 0-100). adapter.log.info(`Battery level ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); saveValue = true; break; case 'I_TIME': // 1 Sensors can request the current time from the Controller using this message. The time will be reported as the seconds since 1970 adapter.log.info(`Time ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); if (!result[i].ack) { // send response: internal, ack=1 mySensorsInterface.write(`${result[i].id};${result[i].childId};3;1;1;${Math.round(new Date().getTime() / 1000)}`, ip); } break; case 'I_SKETCH_VERSION': case 'I_VERSION': // 2 Used to request gateway version from controller. adapter.log.info(`Version ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); saveValue = true; if (!result[i].ack && result[i].subType === 'I_VERSION') { // send response: internal, ack=1 mySensorsInterface.write(`${result[i].id};${result[i].childId};3;1;2;${adapter.version || 0}`, ip); } break; case 'I_SKETCH_NAME': // 2 Used to request gateway version from controller. adapter.log.info(`Name ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); updateSketchName(result[i].id, result[i].payload); saveValue = true; break; case 'I_INCLUSION_MODE': // 5 Start/stop inclusion mode of the Controller (1=start, 0=stop). adapter.log.info(`inclusion mode ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}` ? 'STARTED' : 'STOPPED'); break; case 'I_CONFIG': // 6 Config request from node. Reply with (M)etric or (I)mperal back to sensor. result[i].payload = result[i].payload === 'I' ? 'Imperial' : 'Metric'; adapter.log.info(`Config ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); config[ip || 'serial'] = config[ip || 'serial'] || {}; config[ip || 'serial'].metric = result[i].payload; saveValue = true; break; case 'I_LOG_MESSAGE': // 9 Sent by the gateway to the Controller to trace-log a message adapter.log.debug(`Log ${ip ? ' from ' + ip + ' ' : ''}:${result[i].payload}`); break; case 'I_ID_REQUEST': if (inclusionOn) { // find maximal index let maxId = 0; for (const _id in devices) { if (!devices.hasOwnProperty(_id)) { continue; } if (devices[_id].native && (!ip || ip === devices[_id].native.ip) && parseInt(devices[_id].native.id, 10) > parseInt(maxId, 10)) { maxId = devices[_id].native.id; } } maxId++; if (!result[i].ack) { // send response: internal, ack=0, I_ID_RESPONSE mySensorsInterface.write(`${result[i].id};${result[i].childId};3;0;4;${maxId}`, ip); } } else { adapter.log.warn('Received I_ID_REQUEST, but inclusion mode is disabled'); } break; default: adapter.log.info(`Received INTERNAL message: ${result[i].subType}: ${result[i].payload}`); } if (saveValue) { saveResult(id, result[i], ip, true); if (result[i].subType === 'I_HEARTBEAT_RESPONSE' || result[i].subType === 'I_PRE_SLEEP_NOTIFICATION') { adapter.log.debug('Send unsent values'); findObjAckFalse(ip, result[i].id); } } } else if (result[i].type === 'stream') { switch (result[i].subType) { case 'ST_FIRMWARE_CONFIG_REQUEST': break; case 'ST_FIRMWARE_CONFIG_RESPONSE': break; case 'ST_FIRMWARE_REQUEST': break; case 'ST_FIRMWARE_RESPONSE': break; case 'ST_SOUND': break; case 'ST_IMAGE': break; } } } }); mySensorsInterface.on('connectionChange', (isConn, ip, port) => { adapter.setState('info.connection', isConn, true); // try soft request if (!presentationDone && isConn) { // request metric system mySensorsInterface.write('0;0;3;0;6;get metric', ip, port); mySensorsInterface.write('0;0;3;0;19;force presentation', ip, port); setTimeout(() => { // send reboot command if still no presentation if (!presentationDone) { mySensorsInterface.write('0;0;3;0;13;force restart', ip, port); } }, 1500); } }); }); }); } // If started as allInOne mode => return function to create instance // @ts-ignore if (module.parent) { module.exports = startAdapter; } else { // or start the instance directly startAdapter(); }