UNPKG

iobroker.oppoplayer

Version:
1,027 lines (926 loc) 37.7 kB
/** * * oppoplayer adapter */ /* 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'); const net = require('net'); // import net const named = require('named-js-regexp'); const dgram = require('dgram'); // you have to call the adapter function and pass a options object // name has to be set and has to be equal to adapters folder name and main file name excluding extension // adapter will be restarted automatically every time as the configuration changed, e.g system.adapter.oppoplayer.0 let adapter; const client = new net.Socket(); const parseTime = (player, data) => { data.h = parseInt(data.h); data.s = parseInt(data.s); data.m = parseInt(data.m); data.seconds = data.s + data.m * 60 + data.h * 3600; return data; }; const formatTime = (seconds) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return [ h > 0 ? h : '0' + h, m > 9 ? m : '0' + m, s > 9 ? s : '0' + s, ].filter(a => a).join(':'); }; const atoi = (addr) => { if (!addr) return -1; let parts = addr.split('.').map(function(str) { return parseInt(str); }); return (parts[0] ? parts[0] << 24 : 0) + (parts[1] ? parts[1] << 16 : 0) + (parts[2] ? parts[2] << 8 : 0) + parts[3]; }; const CommandPrefix = "#"; const AnswerPrefix = "@"; // Constants & Variables let host; let pollInterval; let requestInterval; let responseInterval; let pollingVar = false; let connectingVar = null; let isPlayerOnline = false; /** * OPPO doesn't response correctly on every command in case of error * @type {Array} */ let commandQuere = []; let queryCommands = { 'QPW': { // Query power status' poll: false, response: '(?:QPW )?OK ([a-zA-Z]+)', updateResponse: 'UPW ([01])', state: 'info.online', handle: (player, state) => { isPlayerOnline = state === 'ON' || state === '1'; adapter.setState('info.online', isPlayerOnline, true); if (isPlayerOnline) { sendRequest('SVM', '3'); updatePowerOnStates(); // Update states when player is on } else { commandQuere = [] } }, setterFor:['info.online'], setState: (value) => { if (value) { sendRequest('POF'); } else { sendRequest('PON'); } } }, 'QVM': { // Query verbose mode', state: 'settings.verbose', poll: false, response: '(?:QVM )?OK ([0-9])' }, 'QVR': { state: 'settings.firmware', poll: false, pollOnStart: true, response: '(?:QVR )?OK ([a-zA-Z0-9 \-]+)' }, 'QVL': { response: '(?:QVL )?OK ([a-zA-Z0-9]+)', updateResponse: 'UVL ([a-zA-Z0-9]+)', poll: false, pollOnStart: true, handle: (player, volume) => { if (volume === 'MUT' || volume === 'MUTE' || volume === 'UMT') { adapter.setState('settings.mute', volume !== 'UMT', true); return 0; } adapter.setState('settings.mute', false, true); adapter.setState('settings.volume', parseInt(volume), true); return parseInt(volume); }, setterFor: ['settings.mute', 'settings.volume'], setState: (value) => { sendRequest('SVL', value); } }, 'QHD': { state: 'status.resolution', response: '(?:QHD )?OK ([a-zA-Z\ ]+)', updateResponse: 'UVO ([^\\r]+)', pollOnStart: true, setterFor: [], setState: (value) => { sendRequest('SHD', value); } }, 'QPL': { state: 'status.playback', response: '(?:QPL )?OK ([a-zA-Z ]+)', updateResponse: 'UPL ([a-zA-Z ]+)', pollOnStart: true }, 'QTK': { response: '(?:QTK )?OK (?<text>(?<title>[0-9]+)/(?<total>[0-9]+))', poll: true, handle: (command, data) => { adapter.setState('status.title', data.text, true); adapter.setState('status.title.current', parseInt(data.title), true); adapter.setState('status.title.total', parseInt(data.total), true); }, setterFor: ['status.title.current'], setState: (id, value) => { sendRequest("SRH", "T" + value) } }, 'QCH': { desc: 'Query Chapter', response: '(?:QCH )?OK (?<text>(?<chapter>[0-9]+)/(?<total>[0-9]+))', poll: true, handle: (player, data) => { adapter.setState('status.chapter', data.text, true); adapter.setState('status.chapter.current', parseInt(data.chapter), true); adapter.setState('status.chapter.total', parseInt(data.total), true); }, setterFor: ['status.chapter.current'], setState: (id, value) => { sendRequest("SRH", "C" + value) } }, 'QTE': { desc: 'Query Track/Title elapsed time', response: '(?:QTE )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC (?<title>[^\ ]+) (?<chapter>[^\ ]+) E (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status !== 'ER') { parseTime(player, data); adapter.setState('status.elapsed.title', data.time, true); adapter.setState('status.elapsed.title.h', data.h, true); adapter.setState('status.elapsed.title.m', data.m, true); adapter.setState('status.elapsed.title.s', data.s, true); adapter.setState('status.elapsed.title.seconds', data.seconds, true); } else { adapter.setState('status.elapsed.title', false, true); adapter.setState('status.elapsed.title.h', false, true); adapter.setState('status.elapsed.title.m', false, true); adapter.setState('status.elapsed.title.s', false, true); adapter.setState('status.elapsed.title.seconds', false, true); } }, setterFor: ['status.elapsed.title', 'status.elapsed.title.seconds'], setState: (id, value) => { sendRequest("SRH", "T " + (parseInt(value) == value ?formatTime(value) : value )); } }, 'QTR': { desc: 'Query Track/Title remaining time', response: '(?:QTR )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC (?<title>[^ ]+) (?<chapter>[^ ]+) X (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status !== 'ER') { parseTime(player, data); adapter.setState('status.remaining.title', data.time, true); adapter.setState('status.remaining.title.h', data.h, true); adapter.setState('status.remaining.title.m', data.m, true); adapter.setState('status.remaining.title.s', data.s, true); adapter.setState('status.remaining.title.seconds', data.seconds, true); } else { adapter.setState('status.remaining.title', false, true); adapter.setState('status.remaining.title.h', false, true); adapter.setState('status.remaining.title.m', false, true); adapter.setState('status.remaining.title.s', false, true); adapter.setState('status.remaining.title.seconds', data.seconds, true); } } }, 'QCE': { desc: 'Query Chapter elapsed time', response: '(?:QCE )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC (?<title>[^ ]+) (?<chapter>[^ ]+) C (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status !== 'ER') { parseTime(player, data); adapter.setState('status.elapsed.chapter', data.time, true); adapter.setState('status.elapsed.chapter.h', data.h, true); adapter.setState('status.elapsed.chapter.m', data.m, true); adapter.setState('status.elapsed.chapter.s', data.s, true); adapter.setState('status.elapsed.chapter.seconds', data.seconds, true); } else { adapter.setState('status.elapsed.chapter', false, true); adapter.setState('status.elapsed.chapter.h', false, true); adapter.setState('status.elapsed.chapter.m', false, true); adapter.setState('status.elapsed.chapter.s', false, true); adapter.setState('status.elapsed.chapter.seconds', false, true); } }, setterFor: ['status.elapsed.chapter', 'status.elapsed.chapter.seconds'], setState: (id, value) => { sendRequest("SRH", "C " + (parseInt(value) == value ?formatTime(value) : value )) } }, 'QCR': { desc: 'Query Chapter remaining time', response: '(?:QCR )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC (?<title>[^ ]+) (?<chapter>[^ ]+) K (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status !== 'ER') { parseTime(player, data); adapter.setState('status.remaining.chapter', data.time, true); adapter.setState('status.remaining.chapter.h', data.h, true); adapter.setState('status.remaining.chapter.m', data.m, true); adapter.setState('status.remaining.chapter.s', data.s, true); adapter.setState('status.remaining.chapter.seconds', data.seconds, true); } else { adapter.setState('status.remaining.chapter', false, true); adapter.setState('status.remaining.chapter.h', false, true); adapter.setState('status.remaining.chapter.m', false, true); adapter.setState('status.remaining.chapter.s', false, true); adapter.setState('status.remaining.chapter.seconds', false, true); } } }, 'QEL': { desc: 'Query Total elapsed time', response: '(?:QEL )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC\ (?<title>[^\ ]+)\ (?<chapter>[^\ ]+)\ T (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status !== 'ER') { parseTime(player, data); adapter.setState('status.elapsed.total', data.time, true); adapter.setState('status.elapsed.total.h', data.h, true); adapter.setState('status.elapsed.total.m', data.m, true); adapter.setState('status.elapsed.total.s', data.s, true); adapter.setState('status.elapsed.total.seconds', data.seconds, true); } else { adapter.setState('status.elapsed.total', false, true); adapter.setState('status.elapsed.total.h', false, true); adapter.setState('status.elapsed.total.m', false, true); adapter.setState('status.elapsed.total.s', false, true); adapter.setState('status.elapsed.total.seconds', false, true); } }, setterFor: ['status.elapsed.total', 'status.elapsed.total.seconds'], setState: (id, value) => { sendRequest("SRH", (parseInt(value) == value ?formatTime(value) : value )); } }, 'QRE': { desc: 'Query Total remaining time', response: '(?:QRE )(?<status>OK (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))|ER)', updateResponse: 'UTC (?<title>[^ ]+) (?<chapter>[^ ]+) R (?<time>(?<h>[0-9][0-9]):(?<m>[0-9][0-9]):(?<s>[0-9][0-9]))', poll: true, handle: (player, data) => { if (data.status.startsWith('OK')) { parseTime(player, data); adapter.setState('status.remaining.total', data.time, true); adapter.setState('status.remaining.total.h', data.h, true); adapter.setState('status.remaining.total.m', data.m, true); adapter.setState('status.remaining.total.s', data.s, true); adapter.setState('status.remaining.total.seconds', data.seconds, true); } else { adapter.setState('status.remaining.total', false, true); adapter.setState('status.remaining.total.h', false, true); adapter.setState('status.remaining.total.m', false, true); adapter.setState('status.remaining.total.s', false, true); adapter.setState('status.remaining.total.seconds', false, true); } } }, 'QDT': { desc: 'Query disc type', state: 'status.disc', response: '(?:QDT )?OK ([a-zA-Z \-]+)', updateResponse: 'UDT ([a-zA-Z -]+)', pollOnStart: true }, 'QAT': { desc: 'Query audio type', response: '(?:QAT )?OK (?<text>(?<type>[^ ]+) (?<track>[0-9]+)/(?<total>[0-9]+)(?: (?<language>[A-Za-z]*)))', updateResponse: 'UAT (?<text>(?<type>[^ ]+) (?<track>[0-9]+)/(?<total>[0-9]+)(?: (?<language>[A-Za-z]*)))', pollOnStart: true, handle: (player, data) => { adapter.setState("status.audio", data.text, true); adapter.setState("status.audio.type", data.type, true); adapter.setState("status.audio.track", data.track, true); adapter.setState("status.audio.total", data.total, true); adapter.setState("status.audio.language", data.language, true); } }, 'QST': { desc: 'Query subtitle type', response: '(?:QST )?OK (?<text>OFF|(?<number>[0-9]+)/(?<total>[0-9]+)(?: (?<language>[A-Za-z]*)))', updateResponse: 'UST (?<text>OFF|(?<number>[0-9]+)/(?<total>[0-9]+)(?: (?<language>[A-Za-z]*)))', pollOnStart: true, handle: (player, data) => { adapter.setState("status.subtitle", data.text, true); adapter.setState("status.subtitle.number", data.number, true); adapter.setState("status.subtitle.total", data.total, true); adapter.setState("status.subtitle.language", data.language, true); } }, 'QSH': { desc: 'Query subtitle shift', state: 'status.subtitle.shift', response: '(?:QSH )?OK ([0-9\-]+)', pollOnStart: true, setterFor: ['status.subtitle.shift'], setState: (id, value) => { let val = parseInt(value); if (val >= -10 && val <= 10) { sendRequest("SSH", value); } } }, 'QOP': { desc: 'Query OSD position', state: 'status.osd', response: '(?:QOP )?OK ([0-5]+)', pollOnStart: true, setterFor: ['status.osd'], setState: (id, value) => { let val = parseInt(value); if (val >= -10 && val <= 10) { sendRequest("SOP", value); } } }, 'QRP': { desc: 'Query Repeat Mode', state: 'status.repeat', response: '(?:QRP )?OK (?<number>[0-5][0-5]) (?<text>[A-Za-z \-]+)', pollOnStart: true, handle: (player, data) => { adapter.setState('status.repeat', data.number, true) }, setterFor: ['status.repeat'], setState: (id, value) => { const params = { "00": "OFF", "01": "ONE", "02": "CH", "03": "ALL", "04": "TT", "05": "SHF", "06": "RND" }; if (typeof params[value] === 'string') { sendRequest('SRP', params[value]); } else { adapter.log.error('[COMMAND] valid repeat mode ' + value); } sendRequest('QRP'); // cquery after update } }, 'QZM': { desc: 'Query Zoom Mode', response: '(?:QZM )?OK (?<number>[0-5][0-5]) (?<text>[A-Za-z \-]+)', pollOnStart: true, handle: (player, data) => { adapter.setState('status.zoom', data.number, true) }, setterFor: ['status.zoom'], setState: (id, value) => { const params = { "00": "1", "01": "AR", "02": "FS", "03": "US", "04": "1.2", "05": "1.3", "06": "1.5", "07": "2", "08": "3", "09": "4", "10": "1/2", "11": "1/3", "12": "1/4" }; if (typeof params[value] === 'string') { sendRequest('SZM', params[value]); } else { adapter.log.error('[COMMAND] valid zoom mode ' + value); } sendRequest("QZM"); } }, // added with UDP20X-54-1127 'QHS': { desc: 'Query HDR Status', state: 'status.hdr', response: '(?:QHS )?OK ([a-zA-Z\ ]+)', pollOnStart: true, poll: true }, 'QHR': { desc: 'Query HDR Settings', state: 'settings.hdr', response: '(?:QHR )?OK ([a-zA-Z\ ]+)', pollOnStart: true, poll: true, setterFor: ['settings.hdr'], setState: (id, value) => { sendRequest("SHR", value); } }, 'QIS': { desc: 'Query Input Source', state: 'settings.input', response: '(?:QIS )?OK (?<number>[0-9]) (?<name>[a-zA-Z ]+)', updateResponse: 'UIS (?<number>[0-9]) (?<name>[a-zA-Z ]+)', handle: (player, data) => { adapter.setState('settings.input', data.number, true) }, pollOnStart: true, setterFor: ['settings.input'], setState: (id, value) => { sendRequest("SIS", value); } }, 'QAR': { desc: 'Query aspect ratio setting', state: 'status.aspect', response: '(?:QAR )?OK ([0-9a-zA-Z\ ]+)', updateResponse: 'UAR ([0-9a-zA-Z ]+)', pollOnStart: true }, 'Q3D': { desc: 'Query 3D Status', state: 'status.3d', response: '(?:Q3D )?OK ([0-9a-zA-Z\ ]+)', updateResponse: 'U3D ([0-9a-zA-Z ]+)', pollOnStart: true } }; let setCommands = { 'NOP': { response: 'OK' }, 'SVM': { response: '(?:SVM )?OK (\\d)', queryCommand: 'QVM' }, 'POW': { response: '(?:POW )?OK ([a-zA-Z]+)', queryCommand: 'QPW' }, 'PON': { response: 'OK (ON)', queryCommand: 'QPW' }, 'POF': { response: 'OK (OFF)', queryCommand: 'QPW' }, 'SHD': { response: 'OK ([a-zA-Z0-9_]+)', queryCommand: 'QHD' }, 'SSH': { response: 'SSH OK ([0-9-]+)', queryCommand: 'QSH' }, 'SOP': { response: 'SOP OK ([0-9-]+)', queryCommand: 'QOP' }, 'SZM': { response: 'SZM OK ([a-zA-Z0-9_]+)', queryCommand: 'QZM' }, 'SVL': { response: 'SVL OK ([a-zA-Z0-9_]+)', queryCommand: 'QVL' }, 'SRP': { response: 'SRP OK ([a-zA-Z0-9_]+)', queryCommand: 'QRP' }, 'SRH': { response: 'SRP OK' }, 'DPL': { response: 'DPL OK' }, 'RST': { response: 'RST OK' }, 'STC': { response: 'STC OK ([ERTXCK])' }, 'SHR': { response: 'SRP OK ([a-zA-Z]+)', queryCommand: 'QHR' }, 'SIS': { response: 'SRP OK (?<number>[0-9]) (?<name>[a-zA-Z ]+)', queryCommand: 'QIS' }, 'SSA': { response: 'SRP OK ([a-zA-Z]+)' }, 'APP': { response: 'APP OK ([a-zA-Z]+)' }, 'SSD': { response: 'SSD OK ([MSC]+)' }, 'SDP': { response: 'SDP OK ([DPA]+)' }, 'FWD': { response: 'FWD OK ([0-9\/]+)' }, 'REV': { response: 'REV OK ([0-9/]+)' }, 'QDR': { /* unimplemented */} }; // some caching stuff let updateCommands = {}; let setter = {}; Object.keys(queryCommands).forEach(function (key) { let command = queryCommands[key]; queryCommands[key].poll = command.poll || false; queryCommands[key].pollOnStart = command.pollOnStart || false; if (command.response) { command.responseRegEx = named('^' + command.response); } else { command.responseRegEx = named('^' + key); } if (command.updateResponse) { let updateResponse = command.updateResponse; let updateCommand = updateResponse.substr(0, 3); if (!updateCommands[updateCommand]) { updateCommands[updateCommand] = [] } updateCommands[updateCommand].push({ queryCommand: key, regexp: named('^' + updateResponse), handle: command.handle, state: command.state || false }); } if (command.setterFor && command.setState) { command.setterFor.forEach(state => { setter[state] = command.setState; }) } }); Object.keys(setCommands).forEach(function (key) { let command = setCommands[key]; if (command.response) { command.responseRegEx = named('^' + command.response); } else { command.responseRegEx = named('^OK'); } }); let players = {}; let list = []; let oppoDetect = null; function stopOppoDetection() { if (oppoDetect !== null) { oppoDetect.close(); oppoDetect = null; } } function startOppoDetection() { adapter.log.info('[OPPO] auto discovering starting'); oppoDetect = dgram.createSocket('udp4'); oppoDetect.on('close', function () { adapter.log.info('[OPPO] auto discovering stopped'); }); oppoDetect.on('error', (err) => { adapter.log.error("[OPPO] discovering server error: " + err.message); oppoDetect.close(); oppoDetect = null; }); let playerIp = -1; let friendlyName = ""; adapter.getState("info.friendlyName", (err, state) => { friendlyName = (state && state.ack ? state.val : ""); adapter.getState("info.ip", (err, state) => { playerIp = (state && state.ack ? atoi(state.val) : -1); if (!friendlyName && typeof players[playerIp] !== 'undefined') { // write name of player adapter.setState("info.friendlyName", players[playerIp].name, true); } }); }); oppoDetect.on('message', function (msg, rinfo) { let content = msg.toString(); // checking for OPPO Header if (content.startsWith('Notify:OPPO Player Start')) { let lines = content.split('\n'); let ip = lines[1].split(':')[1] || ""; if (ip !== "" && typeof players[ip] === 'undefined') { let playerName; if (lines.length > 3) { // OPPO 10x dosn't submit player name playerName = lines[3].split(':')[1]; } else { playerName = "OPPO 10x (" + ip + ")" } if (!friendlyName && atoi(ip) === playerIp) { // write name of player adapter.setState("info.friendlyName", playerName, true); friendlyName = playerName; } list.push(players[ip] = { 'ip': ip, 'port': lines[2].split(':')[1], 'name': playerName }); } //adapter.sendTo(obj.from, obj.command, {error: null, list: list}, obj.callback); } }); oppoDetect.on('listening', function () { oppoDetect.setBroadcast(true); adapter.log.info('[OPPO] auto discovering started'); }); oppoDetect.bind(7624); } function main() { adapter.subscribeStates('*'); adapter.setState('info.ip', host, true); connect(); } client.on('timeout', () => { commandQuere = []; pollingVar = false; adapter.log.debug('Player timed out due to no response'); adapter.setState('info.connection', false, true); adapter.setState('info.online', false, true); client.destroy(); client.unref(); connectingVar = setTimeout(() => connect(), 30000); // Connect again in 30 seconds }); // Connection handling client.on('error', error => { adapter.setState('info.connection', false, true); adapter.setState('info.online', false, true); commandQuere = []; if (connectingVar) return; if (error.code === 'ECONNREFUSED') adapter.log.debug('Connection refused, make sure that there is no other Telnet connection'); else if (error.code === 'EHOSTUNREACH') adapter.log.debug('Player unreachable, check the Network Config of your OPPO player'); else if (error.code === 'EALREADY' || error.code === 'EISCONN') adapter.log.debug('Adapter is already connecting/connected'); else adapter.log.debug('Connection closed: ' + error); pollingVar = false; if (!connectingVar) { client.destroy(); client.unref(); connectingVar = setTimeout(() => connect(), 30000); // Connect again in 30 seconds } // endIf }); client.on('end', () => { // Oppo has closed the connection adapter.log.warn('OPPO player has cancelled the connection'); commandQuere = []; pollingVar = false; adapter.setState('info.connection', false, true); adapter.setState('info.online', false, true); if (!connectingVar) { client.destroy(); client.unref(); connectingVar = setTimeout(() => connect(), 30000); // Connect again in 30 seconds } // endIf }); client.on('connect', () => { // Successfull connected clearTimeout(connectingVar); connectingVar = null; adapter.setState('info.connection', true, true); adapter.log.info('[CONNECT] Adapter connected to OPPO player: ' + host + ':23'); sendRequest('QPW'); // testing if OPPO powered }); client.on('data', data => { // split data by <cr> const dataArr = data.toString().split(/[\r\n]+/); // Split by Carriage Return for (let i = 0; i < dataArr.length; i++) { if (dataArr[i]) { // dataArr[i] contains element adapter.log.silly('<== ' + dataArr[i]); handleResponse(dataArr[i]); } // endIf } // endFor sendToClient(); }); /** * Internals */ function connect() { adapter.setState('info.connection', false, true); adapter.setState('info.online', false, true); client.setTimeout((pollInterval > 0 ? pollInterval * 2 : 30000)); adapter.log.debug('Trying to connect to ' + host + ':23'); connectingVar = null; client.connect({port: 23, host: host}); } // endConnect let pollOnStartCommands = Object.keys(queryCommands).filter((key) => { return queryCommands[key].pollOnStart || false; }); let pollCommands = Object.keys(queryCommands).filter((key) => { return queryCommands[key].poll || false; }); function updatePowerOnStates() { adapter.log.debug('Connected --> updating states'); if (pollOnStartCommands.length > 0 && isPlayerOnline) { let i = 0; let intervalVar = setInterval(() => { sendRequest(pollOnStartCommands[i]); i++; if (i === pollOnStartCommands.length) clearInterval(intervalVar); }, requestInterval); } } // endUpdateStates function pollStates() { // Polls states if (pollCommands.length > 0 && isPlayerOnline) { let i = 0; pollingVar = false; let intervalVar = setInterval(() => { sendRequest(pollCommands[i]); i++; if (i === pollCommands.length) clearInterval(intervalVar); }, requestInterval); } } // endPollStates function sendRequest(cmd, param) { const now = Date.now(); // remove first commands if older than [responseInterval]sec // improve this while (commandQuere.length > 0 && commandQuere[0].timestamp !== null && commandQuere[0].timestamp + responseInterval < now) { commandQuere.shift(); } // player doesn't answer on most commands while being off. Don't queue them if (isPlayerOnline || cmd === 'QPW' || cmd === 'POW' || cmd === 'PON' ) { param = param || null; commandQuere.push({ 'name': cmd, 'parameter': param, 'timestamp': null // send timestemp }); adapter.log.silly('=== ' + cmd + (param !== null ? ' ' + param : '')); sendToClient(); } } // endSendRequest function sendToClient() { //return new Promise(resolve => { if (commandQuere.length >= 1) { let command = commandQuere[0]; if (command.timestamp === null) { // command not send command.timestamp = Date.now(); adapter.log.debug('==> ' + CommandPrefix + command.name + (command.parameter !== null ? ' ' + command.parameter : '')); client.write(CommandPrefix + command.name + (command.parameter !== null ? ' ' + command.parameter : '') + "\r\n"); } else { const now = Date.now(); while (commandQuere.length > 0 && commandQuere[0].timestamp !== null && commandQuere[0].timestamp + responseInterval < now) { commandQuere.shift(); } } } // resolve(); // }); } function handleResponse(data) { adapter.log.debug("<== " + data); adapter.log.debug(pollingVar + " / " + isPlayerOnline + " / " + pollInterval); if (!pollingVar && isPlayerOnline) { pollingVar = true; if (pollInterval > 0) { // Keep connection alive & poll states setTimeout(() => pollStates(), pollInterval); // Poll states every configured seconds } else if (pollInterval === 0) { // Keep connection alive without polling setTimeout(() => { sendRequest('NOP');pollingVar = false;} , 15000); } } // get command out of String if (data.startsWith(AnswerPrefix)) { let answer = data.substr(1); let matched = null; if (answer.startsWith('U')) { // receive update response let updateCommand = answer.substr(0, 3); if (updateCommands[updateCommand]) { // try to match response for (let i = 0; i < updateCommands[updateCommand].length; i++) { let matched = answer.match(updateCommands[updateCommand][i].regexp); if (!matched) continue; let handle = updateCommands[updateCommand][i].handle || ((command, data) => setState(command, data)); if (Object.keys(matched.groups()).length > 0) { // names groups handle(updateCommands[updateCommand][i], matched.groups()) } else { handle(updateCommands[updateCommand][i], matched[1]) } break; } } } else if (commandQuere.length > 0) { let command = commandQuere[0].name; let queryCommand = queryCommands[command]; if (queryCommand) { commandQuere.shift(); matched = answer.match(queryCommand.responseRegEx); if (matched) { let handle = queryCommand.handle || ((command, data) => setState(command, data)); if (Object.keys(matched.groups()).length > 0) { // names groups handle(queryCommand, matched.groups()) } else { handle(queryCommand, matched[1]); } } } else { let setCommand = setCommands[command]; if (setCommand) { commandQuere.shift(); matched = answer.match(setCommand.responseRegEx); if (matched && setCommand.queryCommand) { let handle = queryCommands[setCommand.queryCommand].handle || ((command, data) => setState(command, data)); if (Object.keys(matched.groups()).length > 0) { // names groups handle(queryCommands[setCommand.queryCommand], matched.groups()); } else { handle(queryCommands[setCommand.queryCommand], matched[1]); } } } } } else { } } sendToClient(); } // endHandleResponse function setState(queryCommand, value) { if (queryCommand.state) { adapter.setState(queryCommand.state, value, true); } } function startAdapter(options) { options = options || {}; Object.assign(options, { name: 'oppoplayer', }); adapter = new utils.Adapter(options); // is called when adapter shuts down - callback has to be called under any circumstances! adapter.on('unload', callback => { try { adapter.log.info('[END] Stopping Oppo player adapter...'); adapter.setState('info.connection', false, true); adapter.setState('info.online', false, true); client.destroy(); // kill connection client.unref(); // kill connection stopOppoDetection(); callback(); } catch (e) { callback(); } // endTryCatch }); // Some message was sent to adapter instance over message box. Used by email, pushover, text2speech, ... adapter.on('message', function (obj) { if (typeof obj === 'object') { if (obj.command === 'browse') { if (obj.callback) { adapter.sendTo(obj.from, obj.command, {error: null, list: list}, obj.callback); } // endIf } // endIf } }); // is called when databases are connected and adapter received configuration. // start here! adapter.on('ready', function () { startOppoDetection(); if (adapter.config.ip) { adapter.log.info('[START] Starting OPPO player adapter'); // init host = adapter.config.ip; pollInterval = parseInt(adapter.config.pollInterval || 5000); requestInterval = parseInt(adapter.config.requestInterval || 100); responseInterval = parseInt(adapter.config.responseInterval || 1000); main(); } else adapter.log.warn('No IP-address set'); }); // Handle state changes adapter.on('stateChange', (id, state) => { if (!id || !state || state.ack || state.from === 'system.adapter.' + adapter.namespace) return; // Ignore acknowledged state changes or error states or local changes id = id.substring(adapter.namespace.length + 1); // remove instance name and id state = state.val; // only get state value adapter.log.debug('[COMMAND] State Change - ID: ' + id + '; State: ' + state); if (typeof setter[id] === 'function') { setter[id](id, state); } else if (id.startsWith('remote')) { if (id === 'remote._cmd') { sendRequest(state.toUpperCase()); adapter.setState(id, '', true); } else sendRequest(id.substring(7).toUpperCase()) } }); // endOnStateChange /* Why?? normally unneeded ... and if: needs to go into "on('main') ..." adapter.getForeignObject(adapter.namespace, (err, obj) => { // create device namespace if (!obj) { adapter.setForeignObject(adapter.namespace, { type: 'device', common: { name: 'OPPO player ' } }); } // endIf }); */ return adapter; } // If started as allInOne/compact mode => return function to create instance if (module && module.parent) { module.exports = startAdapter; } else { // or start the instance directly startAdapter(); }