iobroker.elv-sup2
Version:
Connects ELV SUP2 to ioBroker
1,013 lines (946 loc) • 36.1 kB
JavaScript
;
/*
* 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.5,
max: 108.0,
read: true,
write: false,
};
break;
case 'FRE2':
common = {
name: 'Preset Frequency 2',
type: 'number',
role: 'value',
unit: 'MHz',
min: 87.5,
max: 108.0,
read: true,
write: false,
};
break;
case 'FRE3':
common = {
name: 'Preset Frequency 3',
type: 'number',
role: 'value',
unit: 'MHz',
min: 87.5,
max: 108.0,
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.0,
max: 90.0,
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.5,
max: 108.0,
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,
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} 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();
}