UNPKG

iobroker.elv-sup2

Version:
984 lines (893 loc) 24.8 kB
'use strict'; /* * Created with @iobroker/create-adapter v2.1.1 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); const Sup = require('./lib/sup.js'); const { Queue } = require('async-await-queue'); // Load your modules here, e.g.: // const fs = require("fs"); let sup = {}; const objects = {}; const Debug = false; const channelId = 'configuration'; // SUP config channel let connectTimeout; let checkConnectionTimer; //let refreshTimeout; let timeoutId; /** * No more than 1 concurrent tasks with * at least 100ms between two tasks * (measured from task start to task start) */ const scq = new Queue(1, 1000); //state change queue const ocq = new Queue(1,100); //object create queue const ssq = new Queue(1,100); //state set queue const myPriority = -1; // priority -1 is higher priority than 0 //const pscq =[]; //const tqueue = []; //let workingOnPromise = false; //let item = []; //SUP parameters which are not included in response message const supControl = { FREQ: 8850, RDST: 'First text', MODE: 'STEREO', TA: 'OFF', TP: 'OFF', MUTE: 'OFF', RF: 'ON' }; /* // SUP2 command list // https://files2.elv.com/public/09/0910/091048/Internet/91048_sup2_bedienhinweise.pdf const commands = { get: 'GET', inpl: 'INPL', lim: 'LIM', inpm: 'INPM', freq: 'FREQ', adev: 'ADEV', pow: 'POW', pree: 'PREE', rds: 'RDS', rdsy: 'RDSY', rdsp: 'RDSP', ta: 'TA', tp: 'TP', mute: 'MUTE', mode: 'MODE', rf: 'RF', rdst: 'RDST' }; */ const serialformat = /^(COM|com)[0-9][0-9]?$|^\/dev\/tty.*$/; class ElvSup2 extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'elv-sup2', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Initialize your adapter here try { require('serialport').SerialPort; } catch (err) { this.log.warn('serialport module is not available'); if (this.supportsFeature && !this.supportsFeature('CONTROLLER_NPM_AUTO_REBUILD')) { // re throw error to allow rebuild of serialport in js-controller 3.0.18+ throw err; } } // Reset the connection indicator during startup this.setState('info.connection', false, true); let portOk = true; try { portOk = await this.checkPort(); //this.log.info('portOK: ' + portOk); } catch (err) { portOk = false; //this.log.info('portOK: ' + portOk); this.log.error('Cannot open serial port: ' + err.message); return; } if (portOk) { this.connect() .then (()=> this.initObjects() .then (() => this.subscribeStatesAsync(channelId + '.*'))) .catch ((err) => { this.log.error('Cannot connect to SUP: ' + err.message); }); } } // check if serial port is available checkPort() { return new Promise((resolve,reject) => { const { SerialPort } = require('serialport'); const foundPorts = []; // list all available ports SerialPort.list() .then((ports) => { // iterate through ports for (let i = 0; i < ports.length; i += 1) { if (Debug) this.log.debug ('Found serial port: ' + JSON.stringify(ports[i])); foundPorts.push(ports[i].path); } }) .catch (( (error) => { if (Debug) this.log.debug('Serial port list failed: ' + error); reject (error); })) .finally (( () => { if (Debug) this.log.info ('Serial ports found: ' + JSON.stringify(foundPorts)); if (!this.config.connectionIdentifier) { reject (new Error ('Serial port is not selected. Available ports: ' + JSON.stringify(foundPorts))); } else if (!this.config.connectionIdentifier.match(serialformat)) { reject (new Error ('Serial port ID is not valid. Format: /dev/ttyXXX or COMx. Available ports: ' + JSON.stringify(foundPorts))); } else { const sPort = new SerialPort({ path: this.config.connectionIdentifier, baudRate: parseInt(this.config.baudrate, 10), autoOpen: false }); sPort.open(); sPort.on('error', (err) => { if (sPort.isOpen) { sPort.flush(()=>{ sPort.close(); }); } err.message = err.message + '. Available ports: ' + JSON.stringify(foundPorts); reject (err); }); sPort.on( 'open', () => { //this.log.debug('sPort opened: ' + this.config.connectionIdentifier); sPort.isOpen && sPort.flush(()=>{ sPort.close(()=>{ resolve (true); }); }); }); } })); }); } // connect to SUP via serial port connect() { return new Promise((resolve, reject) => { const options = { connectionMode: 'serial' , serialport: this.config.connectionIdentifier, baudrate: parseInt(this.config.baudrate, 10), databits: 8, stopbits: 1, parity: 'even', debug: Debug, parse: true, logger: this.log.debug }; sup = new Sup(options); sup.on('close', (err) => { this.setState('info.connection', false, true); if (err && err.disconnect === true) { connectTimeout = setInterval(() => { this.sup = null; this.log.error(err + ' - Trying to reconnect Sup... '); this.connect().then ( () => { clearInterval(connectTimeout); connectTimeout = null; }) .catch ( (error) => { this.log.error(error + ' - Trying to reconnect Sup... '); }); }, 10000); } }); sup.once('ready', () => { this.setState('info.connection', true, true); this.log.info('SUP connected: ' + JSON.stringify(options)); resolve (true); }); sup.on('error', (err) => { this.setState('info.connection', false, true); //this.log.error('Error on sup connection: ' + err.message); reject (err); }); }); } // In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above. //this.subscribeStates('testVariable'); // You can also add a subscription for multiple states. The following line watches all states starting with "lights." // this.subscribeStates('lights.*'); // Or, if you really must, you can also watch all states. Don't do this if you don't need to. Otherwise this will cause a lot of unnecessary load on the system: // this.subscribeStates('*'); /* setState examples you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd) */ /*// the variable testVariable is set to true as command (ack=false) await this.setStateAsync('testVariable', true); // same thing, but the value is flagged "ack" // ack should be always set to true if the value is received from or acknowledged from the target system await this.setStateAsync('testVariable', { val: true, ack: true }); // same thing, but the state is deleted after 30s (getState will return null afterwards) await this.setStateAsync('testVariable', { val: true, ack: true, expire: 30 }); // examples for the checkPassword/checkGroup functions let result = await this.checkPasswordAsync('admin', 'iobroker'); this.log.info('check user admin pw iobroker: ' + result); result = await this.checkGroupAsync('admin', 'admin'); this.log.info('check group user admin group admin: ' + result); */ /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ async onUnload(callback) { connectTimeout && clearInterval(connectTimeout); connectTimeout = null; checkConnectionTimer && clearTimeout(checkConnectionTimer); checkConnectionTimer = null; //refreshTimeout && clearTimeout(refreshTimeout); //refreshTimeout = null; timeoutId && clearTimeout(timeoutId); timeoutId = null; if (sup && sup.isOpen) { sup.close((e) => { if (e) this.log.error('Cannot close serial port: ' + e.message); }); } callback(); } // If you need to react to object changes, uncomment the following block and the corresponding line in the constructor. // You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`. // /** // * Is called if a subscribed object changes // * @param {string} id // * @param {ioBroker.Object | null | undefined} obj // */ // onObjectChange(id, obj) { // if (obj) { // // The object was changed // this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`); // } else { // // The object was deleted // this.log.info(`object ${id} deleted`); // } // } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ async onStateChange(id, state) { try { await this.processStateChange(id, state); } catch (err) { this.log.error('Cannot process state change: ' + err.message); } } queuedSendCommand(cmd) { return new Promise((resolve, reject) => { const me = Symbol(); scq .wait(me, myPriority) .then(() => {this.sendCommand(cmd).then((ack) => {resolve (ack);});}) .catch((e) => {reject (e);}) .finally(() => scq.end(me)) ; }); } processStateChange(sid, state) { return new Promise((resolve, reject) => { if (Debug) this.log.debug('State Change ' + JSON.stringify(sid) + ', State: ' + JSON.stringify(state)); if (state && !state.ack) { // State Change "elv-sup2.0.Config.inpl" State: {"val":100,"ack":false,"ts":1581365531968,"q":0,"from":"system.adapter.admin.0","user":"system.user.admin","lc":1581365531968} const oCmnd = sid.split('.'); if (oCmnd.length < 4) { reject (new Error ('Invalid object id in processStateChange')); //return; } // 0: elv-sup2; 1:0; 2:Config; 3:inpl; let supCommand = ''; if (oCmnd[2] === channelId) { switch (oCmnd[3]) { case 'INPL': supCommand = '*'+ 'INPL:' + state.val + '\n'; break; case 'LIM': supCommand = '*'+ 'LIM:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'INPM': supCommand = '*'+ 'INPM:' + (state.val.toString().substring(0,1)==='A' ? 'ANALOG' : 'DIGITAL') + '\n'; break; case 'MODE': supCommand = '*'+ 'MODE:' + (state.val.toString().substring(0,1)==='S' ? 'STEREO' : 'MONO') + '\n'; break; case 'FREQ': supCommand = '*'+ 'FREQ:' + state.val*100 + '\n'; break; case 'ADEV': supCommand = '*'+ 'ADEV:' + state.val*100 + '\n'; break; case 'POW': supCommand = '*'+ 'POW:' + state.val + '\n'; break; case 'PREE': state.val = (state.val <= 49 ? 0 : (state.val <= 74 ? 50 : (state.val === 75 ? 75 : 50))); supCommand = '*'+ 'PREE:' + state.val + '\n'; break; case 'RDS': supCommand = '*'+ 'RDS:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'RDSY': supCommand = '*'+ 'RDSY:' + state.val + '\n'; break; case 'RDSP': supCommand = '*'+ 'RDSP:' + state.val + '\n'; break; case 'TA': supCommand = '*'+ 'TA:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'TP': supCommand = '*'+ 'TP:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'MUTE': supCommand = '*'+ 'MUTE:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'RF': supCommand = '*'+ 'RF:' + (state.val===true ? 'ON' : 'OFF' ) + '\n'; break; case 'RDST': supCommand = '*'+ 'RDST:' + state.val.toString().padEnd(32) + '\n'; break; default: reject (new Error (`Write of State ${oCmnd[3]} not implemented`)); break; } this.queuedSendCommand (supCommand) .then ( (ack) => { this.log.debug('Ack ' + ack ); if (ack == '*A') { this.setStateAsync (sid , state.val, true); resolve(ack); } else { reject (new Error ('Unknown acknowledge from SUP2')); } }) .catch ((err) => { reject ('Error in sendCommand: ' + err.message); }); } else { reject (new Error ('Unknown SUP2 parameter')); } } }); } waitForData () { return new Promise((resolve, reject) => { timeoutId = setTimeout(() => reject(new Error ('Command Response Timeout. Wrong serial port?')), 2000); sup.on('data', (data) => { clearTimeout(timeoutId); //this.log.debug('Response to Send command: ' + data); resolve(data); }); }); } /*** * Send a command to the sup module and return response * sendCommand("*INPL:20\n"); * response: "*A" || '{"VERS":11,"FRE1":8850,"FRE2":8751,"FRE3":8752,"POW":118,"INPM":"ANALOG","INPL":18,"PREE":50,"ADEV":9000,"LIM":"ON","RDS":"ON","RDSP":"NDR KULTNDR KULT","RDSY":13,}' * */ sendCommand(cmd) { return new Promise((resolve, reject) => { if (Debug) this.log.debug('Send command: ' + cmd); sup.write(cmd, err => { if (!err) { this.waitForData() .then ( (result) => { //this.log.debug('Response to Send command: ' + result); resolve(result); }) .catch ( (error) => { //this.log.error('Timeout waiting for response: ' + error); reject(error); }); } else { //this.log.error('Cannot write to port: ' + err); reject(err); } }); }); } /*** * Send a command to the sup module * sendRaw("*INPL:20\n"); * async sendRaw(cmd) { // this.log.info('Send RAW command received. ' + cmd); //sup.write('*INPL:20\n'); // Raw command await sup.write(cmd); } */ getSupConfig() { return new Promise((resolve, reject) => { this.sendCommand('*GET:\n') .then ((response) => { if (response !== '*A') { resolve (JSON.parse(response)); } else { reject (new Error ('Could not get SUP config')); }}) .catch ((err) => { reject (err); }); }); } async initObjects() { let supConfig = {}; try { supConfig = await this.getSupConfig(); if (Debug) this.log.debug('In initObjects: ' + JSON.stringify(supConfig)); } catch (err) { if(err) throw err; //rethrow } if (!objects[this.namespace + '.' + channelId]) { //Channel does not yet exist //create new channel const newChannel = { _id: this.namespace + '.' + channelId, type: 'channel', common: { name: 'SUP2 Configuration', type: 'string', }, native: supConfig }; objects[this.namespace + '.' + channelId] = newChannel; try { await this.createObjNotExists (newChannel); } catch (err) { this.log.error(`Error creating channel object ${newChannel._id}:` + err.message); } } try { // create all objects and initialize states await this.createObjects(supConfig); await this.createObjects(supControl); await this.setStates(supConfig); await this.setStates(supControl); } catch (err) { this.log.error( err); } } async createObjects(config) { //const q = []; let common = {}; for (const obj in config) { const me = Symbol(); /* We wait in the line here */ await ocq.wait(me, myPriority); switch(obj) { case 'VERS': common = { name: 'SW Version', type: 'string', role: 'text', unit: '', read: true, write: false }; break; case 'FRE1': common = { name: 'Preset Frequency 1', type: 'number', role: 'value', unit: 'MHz', min: 87.50, max: 108.00, read: true, write: false }; break; case 'FRE2': common = { name: 'Preset Frequency 2', type: 'number', role: 'value', unit: 'MHz', min: 87.50, max: 108.00, read: true, write: false }; break; case 'FRE3': common = { name: 'Preset Frequency 3', type: 'number', role: 'value', unit: 'MHz', min: 87.50, max: 108.00, read: true, write: false }; break; case 'POW': common = { name: 'Output Power', type: 'number', role: 'power.level', unit: 'dB', min: 88, max: 118, read: true, write: true }; break; case 'INPL': common = { name: 'Input Level', type: 'number', role: 'level.input', unit: '%', min: 0, max: 100, read: true, write: true }; break; case 'PREE': common = { name: 'Preemphasis', type: 'number', role: 'value', unit: 'uS', min: 0, max: 75, read: true, write: true }; break; case 'ADEV': common = { name: 'Audio Deviation', type: 'number', role: 'value', unit: 'kHz', min: 0.00, max: 90.00, read: true, write: true }; break; case 'LIM': common = { name: 'Limiter', type: 'boolean', role: 'indicator', read: true, write: true }; break; case 'RDS': common = { name: 'RDS On/Off', type: 'boolean', role: 'indicator', read: true, write: true }; break; case 'INPM': common = { name: 'Input Mode', type: 'string', role: 'indicator', read: true, write: true }; break; case 'RDSP': common = { name: 'RDS Program Name', type: 'string', role: 'text', read: true, write: true }; break; case 'RDST': common = { name: 'RDS Text', type: 'string', role: 'text', read: true, write: true }; break; case 'RDSY': common = { name: 'RDS Program Type', type: 'number', role: 'value', unit: '', min: 0, max: 31, read: true, write: true }; break; case 'FREQ': common = { name: 'Frequency', type: 'number', role: 'value', unit: 'MHz', min: 87.50, max: 108.00, read: true, write: true }; break; case 'MODE': common = { name: 'Mode', type: 'string', role: 'indicator', read: true, write: true }; break; case 'TA': common = { name: 'TA On/Off', type: 'boolean', role: 'indicator', read: true, write: true }; break; case 'TP': common = { name: 'TP On/Off', type: 'boolean', role: 'indicator', read: true, write: true }; break; case 'MUTE': common = { name: 'Mute On/Off', type: 'boolean', role: 'indicator', read: true, write: true }; break; case 'RF': common = { name: 'RF On/Off', type: 'boolean', role: 'indicator', read: true, write: true }; break; default: return (new Error('Unknown sup configuration parameter: ' + obj)); //break; } const newState = { _id: `${this.namespace}.${channelId}.${obj}`, type: 'state', common: common, native: {} }; objects[`${this.namespace}.${channelId}.${obj}`] = newState; this.createObjNotExists (newState) .catch((e) => {return(e);}) .finally(() => ocq.end(me)); } return await ocq.flush(); } async createObjNotExists (newState) { try { const obj = await this.getForeignObjectAsync(newState._id); if (!obj) { //object does not exist - create it! try { await this.setForeignObjectAsync(newState._id, newState); this.log.debug(`Object ${newState._id} created`); } catch (err) { return (err); } } } catch (err) { return (err); } } async setStates(supStates) { let stateVal; for (const state in supStates) { const oid = this.namespace + '.' + channelId + '.' + state; const me = Symbol(); /* We wait in the line here */ await ssq.wait(me, myPriority); switch(state) { case 'VERS': stateVal = supStates.VERS.toString().replace(/(?<=^.{1})/, '.'); break; case 'FRE1': stateVal = supStates.FRE1/100; break; case 'FRE2': stateVal = supStates.FRE2/100; break; case 'FRE3': stateVal = supStates.FRE3/100; break; case 'POW': stateVal = supStates.POW; break; case 'INPL': stateVal = supStates.INPL; break; case 'PREE': stateVal = supStates.PREE; break; case 'ADEV': stateVal = supStates.ADEV/100; break; case 'LIM': stateVal = supStates.LIM === 'ON' ? true : false; break; case 'RDS': stateVal = supStates.RDS === 'ON' ? true : false; break; case 'INPM': stateVal = supStates.INPM === 'ANALOG'? 'Analog' : 'Digital'; break; case 'MODE': stateVal = supStates.MODE === 'STEREO'? 'Stereo' : 'Mono'; break; case 'RDSP': stateVal = supStates.RDSP; break; case 'RDST': stateVal = supStates.RDST; break; case 'RDSY': stateVal = supStates.RDSY; break; case 'FREQ': stateVal = supStates.FREQ/100; break; case 'TA': stateVal = supStates.TA === 'ON' ? true : false; break; case 'TP': stateVal = supStates.TP === 'ON' ? true : false; break; case 'MUTE': stateVal = supStates.MUTE === 'ON' ? true : false; break; case 'RF': stateVal = supStates.RF === 'ON' ? true : false; break; default: return (new Error('Unknown sup configuration state: ' + state)); //break; } if (Debug) this.log.debug(`state ${oid} pushed with stateVal ${stateVal} ${typeof(stateVal)}`); this.setSupState(oid, stateVal) .catch((err) => {return (err);}) .finally(() => ssq.end(me)); } return await ssq.flush(); } setSupState(oid, stateVal) { return new Promise((resolve, reject) => { this.setForeignStateAsync(oid, stateVal, true) .then (() => { if (Debug) this.log.debug(`state ${oid} set with value ${stateVal}`); resolve (true); }) .catch ((err) => { reject (err); }); }); } /* async updateConfigFromDevice() { let supConfig = {}; try { supConfig = await this.getSupConfig(); if (Debug) this.log.debug('In updateDevice: ' + JSON.stringify(supConfig)); } catch (err) { this.log.error('Error in updateDevice: ' + err.toString()); } for (const state in supConfig) { const oid = this.namespace + '.' + channelId + '.' + state; let localState; try { localState = await this.getStateAsync(oid); } catch (err) { return (err); } if (supConfig.state !== localState.val) { try { await this.setSupState(oid,supConfig.state); } catch (err) { return (err); } } } */ // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor. // /** // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... // * Using this method requires "common.messagebox" property to be set to true in io-package.json // * @param {ioBroker.Message} obj // */ onMessage(obj) { //this.log.info(`messaage received: ${JSON.stringify(obj)}`); if (obj) { switch (obj.command) { case 'listPorts': if (obj.callback) { try { const { SerialPort } = require('serialport'); if (SerialPort) { // read all found serial ports SerialPort.list() .then(ports => { //this.log.info(`List of port: ${JSON.stringify(ports)}`); this.sendTo(obj.from, obj.command, ports.map(item => ({label: item.path, value: item.path})), obj.callback); }) .catch(e => { this.sendTo(obj.from, obj.command, [], obj.callback); this.log.error(e); }); } else { this.log.warn('Module serialport is not available'); this.sendTo(obj.from, obj.command, [{label: 'Not available', value: ''}], obj.callback); } } catch (e) { this.sendTo(obj.from, obj.command, [{label: 'Not available', value: ''}], obj.callback); } } break; } } } } if (require.main !== module) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options={}] */ module.exports = (options) => new ElvSup2(options); } else { // otherwise start the instance directly new ElvSup2(); }