UNPKG

homebridge-oppo-udp

Version:
1,022 lines (1,015 loc) 198 kB
"use strict"; // @ts-check const PLATFORM_NAME = 'oppoPlugin'; const PLUGIN_NAME = 'homebridge-oppo-udp'; const net = require("net"); const request = require('http') const OPPO_PORT = 23; const timeout = 2000; const udp = require('dgram'); const { extractCommandCode: extractCommandCodeFromPayload, parseNumericValue: parseNumericValueFromPayload, } = require('./lib/command-utils'); const { SERVICE_IDS, SERVICE_NAMES, } = require('./lib/service-constants'); const { normalizeInputState, normalizePlaybackState, validateRuntimeConfig: validateRuntimeConfigShape, } = require('./lib/runtime-validation'); const IPV4_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; const QUERY_COMMANDS = Object.freeze({ 'VERBOSE MODE': 'QVM', 'POWER STATUS': 'QPW', 'CURRENT RESOLUTION': 'QHD', 'PLAYBACK STATUS': 'QPL', 'AUDIO TYPE': 'QAT', 'HDR STATUS': 'QHS', 'INPUT STATUS': 'QIS', 'VOLUME STATUS': 'QVL', 'MEDIA NAME': 'QFN', 'CHAPTER NUMBER': 'QCH', 'CHAPTER TIME REMAINING': 'QCR', 'CHAPTER TIME ELAPSED': 'QCE', 'MEDIA TIME ELAPSED': 'QEL', 'MEDIA TIME REMAINING': 'QRE', 'MTR': 'MTR', 'FIRMWARE': 'QVR', }); const BUTTON_COMMANDS = Object.freeze({ 'POWER OFF': 'POF', 'VERBOSE MODE 2': 'SVM 2', 'VERBOSE MODE 3': 'SVM 3', 'EJECT': 'EJT', 'CURSOR UP': 'NUP', 'CURSOR DOWN': 'NDN', 'CURSOR LEFT': 'NLT', 'CURSOR RIGHT': 'NRT', 'CURSOR ENTER': 'SEL', 'MENU': 'MNU', 'BACK': 'RET', 'PLAY': 'PLA', 'PAUSE': 'PAU', 'STOP': 'STP', 'PREVIOUS': 'PRE', 'NEXT': 'NXT', 'CLEAR': 'CLR', 'TOP MENU': 'TTL', 'HOME MENU': 'HOM', 'INFO': 'OSD', 'SETUP': 'SET', 'REWIND': 'REV', 'FORWARD': 'FWD', 'BLURAY INPUT': 'SIS 0', 'HDMI IN': 'SIS 1', 'HDMI OUT': 'SIS 2', 'OPTICAL INPUT': 'SIS 3', 'COAXIAL INPUT': 'SIS 4', 'USB AUDIO INPUT': 'SIS 5', 'DIMMER': 'DIM', 'PURE AUDIO': 'PUR', 'GO TO': 'GOT', 'PAGE UP': 'PUP', 'PAGE DOWN': 'PDN', 'POP-UP MENU': 'MNU', 'RED': 'RED', 'GREEN': 'GRN', 'BLUE': 'BLU', 'YELLOW': 'YLW', 'AUDIO': 'AUD', 'SUBTITLE': 'SUB', 'ANGLE': 'ANG', 'ZOOM': 'ZOM', 'SAP': 'SAP', 'AB REPLAY': 'ATB', 'REPEAT': 'RPT', 'PIP': 'PIP', 'RESOLUTION': 'HDM', 'OPTION': 'OPT', '3D': 'M3D', 'PIC': 'SEH', 'HDR': 'HDR', 'SUBTITTLE (HOLD)': 'SUH', 'INFO (HOLD)': 'INH', 'RESOLUTION (HOLD)': 'RLH', 'AV SYNC': 'AVS', 'GAPLESS PLAY': 'GPA', 'INPUT': 'SRC', 'VOLUME UP': 'VUP', 'VOLUME DOWN': 'VDN', 'ELAPTSED TIME': 'STC T', 'RESET': 'RST', }); const TCP_ROUTED_CODES = new Set([ 'RST', 'PON', 'SVM', 'SIS', 'SVL', 'SRH', 'QVM', 'QPW', 'POF', 'QHD', 'QPL', 'QIS', 'QVL', 'QCH', 'QCR', 'QCE', 'QEL', 'QRE', 'QVR', 'EJT', 'QHS', ]); const DEFAULT_CONFIG = Object.freeze({ name: 'Oppo 203', manufacture: 'Oppo', pollingInterval: 1000, modelName: 'UDP-203', serialN: 'B210U71647033894', autoIP: false, inputButtons: false, oppo205: false, volume: false, mediaButtons: false, cursorUpB: false, cursorDownB: false, cursorLeftB: false, cursorRightB: false, cursorEnterB: false, menuB: false, backButtonB: false, clearB: false, topMenuB: false, optionB: false, homeMenuB: false, infoB: false, setupB: false, goToB: false, pageUpB: false, pageDownB: false, popUpMenuB: false, dimmerB: false, pureAudioB: false, redB: false, yellowB: false, blueB: false, audioB: false, greenB: false, subtitleB: false, angleB: false, zoomB: false, sapB: false, abReplayB: false, repeatB: false, pipB: false, resolutionB: false, threeDB: false, pictureB: false, hdrButtonB: false, subtitleHoldB: false, infoHoldB: false, resolutionHoldB: false, avSyncB: false, gaplessPlayB: false, inputB: false, ejectDiscB: false, movieControl: false, chapterControl: false, chapterSelector: false, chinoppo: false, powerB: false, mediaAudioVideoState: false, changeDimmersToFan: false, remainMovieTimer: false, infoToMenu: false, }); const STATELESS_SWITCH_CONFIGS = Object.freeze([ { configKey: 'cursorUpB', propertyName: 'cursorUp', serviceName: 'Cursor Up', uniqueId: SERVICE_IDS.STATELESS_CURSOR_UP, commandName: 'CURSOR UP' }, { configKey: 'cursorDownB', propertyName: 'cursorDown', serviceName: 'Cursor Down', uniqueId: SERVICE_IDS.STATELESS_CURSOR_DOWN, commandName: 'CURSOR DOWN' }, { configKey: 'cursorLeftB', propertyName: 'cursorLeft', serviceName: 'Cursor Left', uniqueId: SERVICE_IDS.STATELESS_CURSOR_LEFT, commandName: 'CURSOR LEFT' }, { configKey: 'cursorRightB', propertyName: 'cursorRight', serviceName: 'Cursor Right', uniqueId: SERVICE_IDS.STATELESS_CURSOR_RIGHT, commandName: 'CURSOR RIGHT' }, { configKey: 'cursorEnterB', propertyName: 'cursorEnter', serviceName: 'Cursor Enter', uniqueId: SERVICE_IDS.STATELESS_CURSOR_ENTER, commandName: 'CURSOR ENTER' }, { configKey: 'menuB', propertyName: 'menu', serviceName: 'Menu', uniqueId: SERVICE_IDS.STATELESS_MENU, commandName: 'MENU' }, { configKey: 'backButtonB', propertyName: 'backButton', serviceName: 'Back', uniqueId: SERVICE_IDS.STATELESS_BACK, commandName: 'BACK' }, { configKey: 'clearB', propertyName: 'clear', serviceName: 'Clear', uniqueId: SERVICE_IDS.STATELESS_CLEAR, commandName: 'CLEAR' }, { configKey: 'topMenuB', propertyName: 'topMenuB', serviceName: 'Top Menu', uniqueId: SERVICE_IDS.STATELESS_TOP_MENU, commandName: 'TOP MENU' }, { configKey: 'optionB', propertyName: 'option', serviceName: 'Option', uniqueId: SERVICE_IDS.STATELESS_OPTION, commandName: 'OPTION' }, { configKey: 'homeMenuB', propertyName: 'homeMenu', serviceName: 'Home Menu', uniqueId: SERVICE_IDS.STATELESS_HOME_MENU, commandName: 'HOME MENU' }, { configKey: 'infoB', propertyName: 'infoButton', serviceName: 'Info', uniqueId: SERVICE_IDS.STATELESS_INFO, commandName: 'INFO' }, { configKey: 'setupB', propertyName: 'setup', serviceName: 'Setup', uniqueId: SERVICE_IDS.STATELESS_SETUP, commandName: 'SETUP' }, { configKey: 'goToB', propertyName: 'goTo', serviceName: 'Go To', uniqueId: SERVICE_IDS.STATELESS_GOTO, commandName: 'GO TO' }, { configKey: 'pageUpB', propertyName: 'pageUp', serviceName: 'Page Up', uniqueId: SERVICE_IDS.STATELESS_PAGE_UP, commandName: 'PAGE UP' }, { configKey: 'pageDownB', propertyName: 'pageDown', serviceName: 'Page Down', uniqueId: SERVICE_IDS.STATELESS_PAGE_DOWN, commandName: 'PAGE DOWN' }, { configKey: 'popUpMenuB', propertyName: 'popUpMenu', serviceName: 'Pop-Up Menu', uniqueId: SERVICE_IDS.STATELESS_POPUP_MENU, commandName: 'POP-UP MENU' }, { configKey: 'mediaButtons', propertyName: 'previous', serviceName: 'Previous', uniqueId: SERVICE_IDS.STATELESS_PREVIOUS, commandName: 'PREVIOUS' }, { configKey: 'mediaButtons', propertyName: 'next', serviceName: 'Next', uniqueId: SERVICE_IDS.STATELESS_NEXT, commandName: 'NEXT' }, { configKey: 'mediaButtons', propertyName: 'rewindButton', serviceName: 'Rewind', uniqueId: SERVICE_IDS.STATELESS_REWIND, commandName: 'REWIND' }, { configKey: 'mediaButtons', propertyName: 'forwardButton', serviceName: 'Forward', uniqueId: SERVICE_IDS.STATELESS_FORWARD, commandName: 'FORWARD' }, { configKey: 'dimmerB', propertyName: 'dimmer', serviceName: 'Dimmer', uniqueId: SERVICE_IDS.STATELESS_DIMMER, commandName: 'DIMMER' }, { configKey: 'pureAudioB', propertyName: 'pureAudio', serviceName: 'Pure Audio', uniqueId: SERVICE_IDS.STATELESS_PURE_AUDIO, commandName: 'PURE AUDIO' }, { configKey: 'redB', propertyName: 'red', serviceName: 'Red', uniqueId: SERVICE_IDS.STATELESS_RED, commandName: 'RED' }, { configKey: 'greenB', propertyName: 'green', serviceName: 'Green', uniqueId: SERVICE_IDS.STATELESS_GREEN, commandName: 'GREEN' }, { configKey: 'blueB', propertyName: 'blue', serviceName: 'Blue', uniqueId: SERVICE_IDS.STATELESS_BLUE, commandName: 'BLUE' }, { configKey: 'yellowB', propertyName: 'yellow', serviceName: 'Yellow', uniqueId: SERVICE_IDS.STATELESS_YELLOW, commandName: 'YELLOW' }, { configKey: 'audioB', propertyName: 'audio', serviceName: 'Audio', uniqueId: SERVICE_IDS.STATELESS_AUDIO, commandName: 'AUDIO' }, { configKey: 'subtitleB', propertyName: 'subtitle', serviceName: 'Subtitle', uniqueId: SERVICE_IDS.STATELESS_SUBTITLE, commandName: 'SUBTITLE' }, { configKey: 'angleB', propertyName: 'angle', serviceName: 'Angle', uniqueId: SERVICE_IDS.STATELESS_ANGLE, commandName: 'ANGLE' }, { configKey: 'zoomB', propertyName: 'zoom', serviceName: 'Zoom', uniqueId: SERVICE_IDS.STATELESS_ZOOM, commandName: 'ZOOM' }, { configKey: 'sapB', propertyName: 'sap', serviceName: 'SAP', uniqueId: SERVICE_IDS.STATELESS_SAP, commandName: 'SAP' }, { configKey: 'abReplayB', propertyName: 'abReplay', serviceName: 'AB Replay', uniqueId: SERVICE_IDS.STATELESS_AB_REPLAY, commandName: 'AB REPLAY' }, { configKey: 'repeatB', propertyName: 'repeat', serviceName: 'Repeat', uniqueId: SERVICE_IDS.STATELESS_REPEAT, commandName: 'REPEAT' }, { configKey: 'pipB', propertyName: 'pip', serviceName: 'PIP', uniqueId: SERVICE_IDS.STATELESS_PIP, commandName: 'PIP' }, { configKey: 'resolutionB', propertyName: 'resolution', serviceName: 'Resolution', uniqueId: SERVICE_IDS.STATELESS_RESOLUTION, commandName: 'RESOLUTION' }, { configKey: 'threeDB', propertyName: 'threeD', serviceName: '3D', uniqueId: SERVICE_IDS.STATELESS_3D, commandName: '3D' }, { configKey: 'pictureB', propertyName: 'picture', serviceName: 'Picture', uniqueId: SERVICE_IDS.STATELESS_PICTURE, commandName: 'PIC' }, { configKey: 'hdrButtonB', propertyName: 'hdrButton', serviceName: 'HDR Button', uniqueId: SERVICE_IDS.STATELESS_HDR_BUTTON, commandName: 'HDR' }, { configKey: 'subtitleHoldB', propertyName: 'subtitleHold', serviceName: 'Subtitle (Hold)', uniqueId: SERVICE_IDS.STATELESS_SUBTITLE_HOLD, commandName: 'SUBTITTLE (HOLD)' }, { configKey: 'infoHoldB', propertyName: 'infoHold', serviceName: 'Info (Hold)', uniqueId: SERVICE_IDS.STATELESS_INFO_HOLD, commandName: 'INFO (HOLD)' }, { configKey: 'resolutionHoldB', propertyName: 'resolutionHold', serviceName: 'Resolution (Hold)', uniqueId: SERVICE_IDS.STATELESS_RESOLUTION_HOLD, commandName: 'RESOLUTION (HOLD)' }, { configKey: 'avSyncB', propertyName: 'avSync', serviceName: 'AV SYNC', uniqueId: SERVICE_IDS.STATELESS_AV_SYNC, commandName: 'AV SYNC' }, { configKey: 'gaplessPlayB', propertyName: 'gaplessPlay', serviceName: 'Gapless Play', uniqueId: SERVICE_IDS.STATELESS_GAPLESS_PLAY, commandName: 'GAPLESS PLAY' }, { configKey: 'inputB', propertyName: 'input', serviceName: 'Input', uniqueId: SERVICE_IDS.STATELESS_INPUT, commandName: 'INPUT' }, { configKey: 'ejectDiscB', propertyName: 'ejectDisc', serviceName: 'Eject-Load Disc', uniqueId: SERVICE_IDS.STATELESS_EJECT_DISC, commandName: 'EJECT', logLabel: 'Eject/Load Disc' }, ]); const COMMAND_NAME_RULES = Object.freeze([ { patterns: ['PON'], label: 'Power On' }, { patterns: ['POF'], label: 'Power Off' }, { patterns: ['SVM 2'], label: 'Verbose Mode 2' }, { patterns: ['SVM 3'], label: 'Verbose Mode 3' }, { patterns: ['NUP'], label: 'Cursor Up' }, { patterns: ['NDN'], label: 'Cursor Down' }, { patterns: ['NLT'], label: 'Cursor Left' }, { patterns: ['NRT'], label: 'Cursor Right' }, { patterns: ['SEL'], label: 'Enter' }, { patterns: ['MNU'], label: 'Menu' }, { patterns: ['RET'], label: 'Back' }, { patterns: ['PAU'], label: SERVICE_NAMES.PAUSE }, { patterns: ['STP'], label: SERVICE_NAMES.STOP }, { patterns: ['PRE'], label: 'Previous Chapter' }, { patterns: ['NXT'], label: 'Next Chapter' }, { patterns: ['CLR'], label: 'Clear' }, { patterns: ['TTL'], label: 'Top Menu' }, { patterns: ['OPT'], label: 'Option' }, { patterns: ['DISC MENU'], label: 'Disc Menu Screen' }, { patterns: ['MEDIA CENTER'], label: 'Media Center Screen' }, { patterns: ['HOME MENU', 'UPL HOME'], label: 'Home Menu Screen' }, { patterns: ['HOM'], label: 'Home Menu' }, { patterns: ['OSD'], label: 'Information' }, { patterns: ['SET'], label: 'Setup' }, { patterns: ['REV'], label: 'Rewind' }, { patterns: ['FWD'], label: 'Forward' }, { patterns: ['SIS 0', 'SIS OK 0', 'QIS OK 0'], label: 'Bluray Input' }, { patterns: ['SIS 1', 'SIS OK 1', 'QIS OK 1'], label: 'HDMI In' }, { patterns: ['SIS 2', 'SIS OK 2', 'QIS OK 2'], label: 'HDMI Out' }, { patterns: ['SIS 3', 'SIS OK 3', 'QIS OK 3'], label: 'Optical In' }, { patterns: ['SIS 4', 'SIS OK 4', 'QIS OK 4'], label: 'Coaxial In' }, { patterns: ['SIS 5', 'SIS OK 5', 'QIS OK 5'], label: 'USB Audio In' }, { patterns: ['DIM'], label: 'Dimmer' }, { patterns: ['PUR'], label: 'Pure Audio' }, { patterns: ['GOT'], label: 'Go To' }, { patterns: ['PUP'], label: 'Page Up' }, { patterns: ['PDN'], label: 'Page Down' }, { patterns: ['RED'], label: 'Red' }, { patterns: ['GRN'], label: 'Green' }, { patterns: ['BLU'], label: 'Blue' }, { patterns: ['YLW'], label: 'Yellow' }, { patterns: ['AUD'], label: 'Audio' }, { patterns: ['SUB'], label: 'Subtitle' }, { patterns: ['ANG'], label: 'Angle' }, { patterns: ['ZOM'], label: 'Zoom' }, { patterns: ['SAP'], label: 'SAP' }, { patterns: ['ATB'], label: 'AB Replay' }, { patterns: ['RPT'], label: 'Repeat' }, { patterns: ['PIP'], label: 'PIP' }, { patterns: ['HDM'], label: 'Resolution' }, { patterns: ['M3D'], label: '3D' }, { patterns: ['SEH'], label: 'Picture' }, { patterns: ['HDR'], label: 'HDR' }, { patterns: ['SUH'], label: 'Subtitle (Hold)' }, { patterns: ['INH'], label: 'Information (Hold)' }, { patterns: ['RLH'], label: 'Resolution (Hold)' }, { patterns: ['AVS'], label: 'AV Sync' }, { patterns: ['GPA'], label: 'Gapless Play' }, { patterns: ['SRC'], label: 'Input' }, { patterns: ['VUP'], label: 'Volume Up' }, { patterns: ['VDN'], label: 'Volume Down' }, { patterns: ['STC T'], label: 'Elapse Time' }, { patterns: ['RST'], label: 'Reset Command Queue' }, { patterns: ['QVM'], label: 'Verbose Mode Status Query' }, { patterns: ['UPL SCSV'], label: 'Screen Saver On' }, { patterns: ['USB IN'], label: 'USB Connected' }, { patterns: ['USB OU'], label: 'USB Disconnected' }, { patterns: ['QPW'], label: 'Power Status Query' }, { patterns: ['QHD'], label: 'Current Resolution Query' }, { patterns: ['QPL'], label: 'Playback Status Query' }, { patterns: ['QAT'], label: 'Audio Type Query' }, { patterns: ['QHS'], label: 'HDR Status Query' }, { patterns: ['QIS'], label: 'Input Status Query' }, { patterns: ['PLA'], label: SERVICE_NAMES.PLAY }, { patterns: ['QVL'], label: 'Volume Status Query' }, { patterns: ['QFN'], label: 'Media Name Query' }, { patterns: ['QCH'], label: 'Chapter Number Query' }, { patterns: ['QCR'], label: 'Chapter Time Remaining Query' }, { patterns: ['QCE'], label: 'Chapter Time Elapsed Query' }, { patterns: ['QEL'], label: 'Media Time Elapsed Query' }, { patterns: ['QRE', 'MTR'], label: 'Media Time Remaining Query' }, { patterns: ['QVR'], label: 'Firmware Query' }, ]); const COMMAND_NAME_NO_SUFFIX_PATTERNS = Object.freeze(['DISC MENU', 'MEDIA CENTER', 'UPL HOME', 'HOME MENU', 'USB IN', 'USB OU']); const QUERY_COMMAND_COOLDOWNS_MS = Object.freeze({ QHS: 5000, QAT: 4000, QFN: 5000, QVL: 3000, QPL: 2500, QIS: 2500, QPW: 2000, QRE: 2500, QEL: 2500, MTR: 2500, QCH: 3500, QCR: 3500, QCE: 3500, }); const PLAYBACK_PLAY_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['UPL HTTP PLAY']), resetCounter: false, setPowerOn: true, logMessage: 'Response: Play Executed by HTTP', profileName: 'http', startDelayMs: 20100, offsets: Object.freeze({ mtr: 12000, audio: 18000, hdr: 20000, media: 15500 }), }), Object.freeze({ patterns: Object.freeze(['UPL PLAY']), resetCounter: false, setPowerOn: true, logMessage: 'Response: Play Executed (UPL)', profileName: 'upl', startDelayMs: 20100, offsets: Object.freeze({ mtr: 12000, audio: 18000, hdr: 20100, media: 15500 }), }), Object.freeze({ patterns: Object.freeze(['OK PLAY', 'PLA OK']), resetCounter: true, setPowerOn: false, logMessage: 'Response: Play Executed (OK)', profileName: 'ok', startDelayMs: 5100, offsets: Object.freeze({ mtr: 800, audio: 1500, hdr: 3000, media: 5000 }), }), ]); const PLAYBACK_PAUSE_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['UPL HTTP PAUS']), resetCounter: false, logMessage: 'Response: Pause Executed by HTTP', }), Object.freeze({ patterns: Object.freeze(['UPL PAUS']), resetCounter: false, logMessage: 'Response: Pause Executed (UPL)', }), Object.freeze({ patterns: Object.freeze(['OK PAUSE', 'PAU OK']), resetCounter: true, logMessage: 'Response: Pause Executed', }), ]); const PLAYBACK_STOP_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['UPL HTTP STOP']), resetCounter: false, logMessage: 'Response: Stop Executed by HTTP', logOnlyWhenPowered: true, hdrResetMode: 'whenPowered', resetOrder: 'movieThenMedia', }), Object.freeze({ patterns: Object.freeze(['UPL STOP']), resetCounter: false, logMessage: 'Response: Stop Executed (UPL)', logOnlyWhenPowered: false, hdrResetMode: 'always', resetOrder: 'mediaThenMovie', }), Object.freeze({ patterns: Object.freeze(['OK STOP', 'STP OK']), resetCounter: true, logMessage: 'Response: Stop Executed', logOnlyWhenPowered: false, hdrResetMode: 'always', resetOrder: 'movieThenMedia', }), ]); const PLAYBACK_UPL_PROGRESS_PATTERNS = Object.freeze(['UPL STPF', 'UPL STPR', 'UPL FFW1', 'UPL FRV1', 'UPL SFW1', 'UPL SRV1', 'UPL MCTR', 'UPL MENUP', 'UPL SCSV']); const PLAYBACK_OK_PROGRESS_PATTERNS = Object.freeze(['OK STEP', 'OK FREV', 'OK FFWD', 'OK SFWD', 'OK SREV', 'ER OVERTIME', 'OK MEDIA', 'OK SETUP', 'OK SCREEN', 'OK DISC']); const PLAYBACK_HOME_MENU_PATTERNS = Object.freeze(['HOME MENU', 'UPL HOME']); const COMMAND_LOG_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['QVM', 'RST', 'QCE', 'QCH', 'QRE']), action: 'debugCommand' }), Object.freeze({ patterns: Object.freeze(['SRH T']), action: 'searchTitleTime' }), Object.freeze({ patterns: Object.freeze(['SRH C']), action: 'searchChapterTime' }), Object.freeze({ patterns: Object.freeze(['SRH']), action: 'searchTime' }), Object.freeze({ patterns: Object.freeze(['SVL']), action: 'volumeChange' }), Object.freeze({ patterns: Object.freeze(['QPW']), action: 'powerStatusQuery' }), Object.freeze({ patterns: Object.freeze(['PON']), action: 'powerOn' }), ]); const HTTP_URL_ROUTE_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['QRE', 'MTR']), endpoint: 'getplayingtime', logMode: 'default' }), Object.freeze({ patterns: Object.freeze(['QPL', 'QIS', 'QPW']), endpoint: 'getglobalinfo', logMode: 'default' }), Object.freeze({ patterns: Object.freeze(['QVL']), endpoint: 'getvolume', logMode: 'default' }), Object.freeze({ patterns: Object.freeze(['QFN']), endpoint: 'getmovieplayinfo', logMode: 'mediaNameQuery' }), Object.freeze({ patterns: Object.freeze(['SIS']), endpoint: 'sendremotekey?%7B%22SRC%22%3A%22SRC%22%7D', logMode: 'inputChange' }), Object.freeze({ patterns: Object.freeze(['QAT']), endpoint: 'getaudiomenulist', logMode: 'default' }), ]); const MEDIA_NAME_EXTENSIONS = Object.freeze(['.iso', '.mkv', '.mp4', '.mp3']); const AUDIO_STATUS_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['DT', 'Dolby TrueHD', 'QAT OK DD']), label: 'Dolby TrueHD (Atmos)' }), Object.freeze({ patterns: Object.freeze(['DD']), label: 'Dolby Digital' }), Object.freeze({ patterns: Object.freeze(['DP', 'Dolby Digital Plus']), label: 'Dolby Digital Plus' }), Object.freeze({ patterns: Object.freeze(['TS']), label: SERVICE_NAMES.DTS }), Object.freeze({ patterns: Object.freeze(['TM', 'DTS-HD']), label: 'DTS HD MA - DTS X' }), Object.freeze({ patterns: Object.freeze(['TH']), label: 'DTS HD High Resolution' }), Object.freeze({ patterns: Object.freeze(['PC']), label: 'LPCM' }), Object.freeze({ patterns: Object.freeze(['MP']), label: 'MPEG Audio' }), Object.freeze({ patterns: Object.freeze(['CD']), label: 'CD Audio' }), Object.freeze({ patterns: Object.freeze(['UNK']), label: 'Unknown' }), ]); const HTTP_AUDIO_STATUS_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['Digital Plus']), label: 'Dolby Digital Plus' }), Object.freeze({ patterns: Object.freeze(['Dolby Digital']), label: 'Dolby Digital' }), Object.freeze({ patterns: Object.freeze(['TrueHD']), label: 'Dolby TrueHD (Atmos)' }), Object.freeze({ patterns: Object.freeze(['DTS-HD High', 'DTS HD High']), label: 'DTS-HD High Resolution' }), Object.freeze({ patterns: Object.freeze(['DTS HD Master', 'DTS HD MA']), label: 'DTS HD MA - DTS X' }), Object.freeze({ patterns: Object.freeze([SERVICE_NAMES.DTS]), label: SERVICE_NAMES.DTS }), Object.freeze({ patterns: Object.freeze(['LPCM']), label: 'LPCM' }), Object.freeze({ patterns: Object.freeze(['MPEG']), label: 'MPEG Audio' }), Object.freeze({ patterns: Object.freeze(['CD Audio']), label: 'CD Audio' }), ]); const LANGUAGE_SELECTOR_RULES = Object.freeze([ Object.freeze({ patterns: Object.freeze(['ENG']), label: 'English' }), Object.freeze({ patterns: Object.freeze(['ARA']), label: 'Arabic' }), Object.freeze({ patterns: Object.freeze(['CAT']), label: 'Catalan' }), Object.freeze({ patterns: Object.freeze(['CHI', 'ZHO']), label: 'Chinese' }), Object.freeze({ patterns: Object.freeze(['CES', 'CZE']), label: 'Czech' }), Object.freeze({ patterns: Object.freeze(['DAN']), label: 'Danish' }), Object.freeze({ patterns: Object.freeze(['DEU', 'GMH', 'GOH']), label: 'German' }), Object.freeze({ patterns: Object.freeze(['DUM', 'DUT']), label: 'Dutch' }), Object.freeze({ patterns: Object.freeze(['EGY']), label: 'Egyptina' }), Object.freeze({ patterns: Object.freeze(['ELL', 'GRC', 'GRE']), label: 'Greek' }), Object.freeze({ patterns: Object.freeze(['FIN']), label: 'Finnish' }), Object.freeze({ patterns: Object.freeze(['FRA', 'FRE', 'FRM', 'FRO']), label: 'French' }), Object.freeze({ patterns: Object.freeze(['HEB']), label: 'Hebrew' }), Object.freeze({ patterns: Object.freeze(['HIN']), label: 'Hindi' }), Object.freeze({ patterns: Object.freeze(['HRV']), label: 'Croatina' }), Object.freeze({ patterns: Object.freeze(['HUN']), label: 'Hungarian' }), Object.freeze({ patterns: Object.freeze(['ICE', 'ISL']), label: 'Icelandic' }), Object.freeze({ patterns: Object.freeze(['ITA']), label: 'Italian' }), Object.freeze({ patterns: Object.freeze(['JPN']), label: 'Japanese' }), Object.freeze({ patterns: Object.freeze(['KOR']), label: 'Korian' }), Object.freeze({ patterns: Object.freeze(['PEO', 'PER']), label: 'Perian' }), Object.freeze({ patterns: Object.freeze(['POL']), label: 'Polish' }), Object.freeze({ patterns: Object.freeze(['POR']), label: 'Portuguese' }), Object.freeze({ patterns: Object.freeze(['RUS']), label: 'Russian' }), Object.freeze({ patterns: Object.freeze(['RON', 'RUN']), label: 'Romanian' }), Object.freeze({ patterns: Object.freeze(['SPA']), label: 'Spanish' }), Object.freeze({ patterns: Object.freeze(['TUR']), label: 'Turkish' }), Object.freeze({ patterns: Object.freeze(['UND']), label: 'Language Undefined' }), Object.freeze({ patterns: Object.freeze(['UNK']), label: 'Unknown' }), ]); const HTTP_REQUEST_TIMEOUT_MS = 4000; /** * @typedef {boolean[]} InputStateVector * Input-state vector where index 0-9 map to Blu-ray/HDMI/Optical/Coaxial/USB routes. */ /** * @typedef {boolean[]} PlaybackStateVector * Playback-state vector in [playing, paused, stopped] order. */ /** * @typedef {{tcpWrites:number,httpRequests:number,skippedQueries:number,coalescedCommands:number,lastMetricsLogAt:number}} RuntimeMetrics */ module.exports = (api) => { api.registerPlatform(PLUGIN_NAME,PLATFORM_NAME, oppoPlatform, true); }; //// Platform///////////////////////////////////////////////////////////////////////////////////////////////// class oppoPlatform { // Initializes the platform plugin lifecycle hooks and cached accessory container. constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = this.api.hap.Service; this.Characteristic = this.api.hap.Characteristic; this.config.name = this.config.name || 'Oppo UDP'; this.config.newPlatformUUID = this.config.newPlatformUUID || false; // this is used to track restored cached accessories this.accessories = []; this.log.debug('Finished initializing platform:', this.config.name); this.api.on('didFinishLaunching', () => { log.debug('didFinishLaunching callback'); this.iniDevice(); }); this.api.on('shutdown', () => { if (this.oppoAccessoryInstance && typeof this.oppoAccessoryInstance.cleanupRuntimeResources === 'function') { this.oppoAccessoryInstance.cleanupRuntimeResources(); } }); } // Registers accessories restored from Homebridge cache for later reconciliation. configureAccessory(accessory) { this.log.info('Loading accessory from cache:', accessory.displayName); this.accessories.push(accessory); } // Builds the external TV accessory instance and publishes it with a deterministic UUID strategy. iniDevice() { if (this.config.newPlatformUUID === false) { this.oppoDevice = { oppoUniqueId: 'AB1212D', oppoDisplayName: `${this.config.name}` }; } if (this.config.newPlatformUUID === true) { this.oppoDevice = { oppoUniqueId: `${this.config.name}AB1212D`, oppoDisplayName: `${this.config.name}` }; this.log.debug('Generationg a new UUID'); } const uuid = this.api.hap.uuid.generate(this.oppoDevice.oppoUniqueId); this.log.debug('Adding new accessory:', this.oppoDevice.oppoDisplayName); const accessory = new this.api.platformAccessory(this.oppoDevice.oppoDisplayName, uuid); accessory.category = 35; accessory.context.device = this.oppoDevice; this.oppoAccessoryInstance = new oppoAccessory(this, accessory); this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]); } // Unregisters a previously published accessory from Homebridge. removeAccessory(accessory) { this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } class oppoAccessory { // Bootstraps runtime state, wires HomeKit services, then starts discovery/TCP sync loops. constructor(platform, accessory) { this.platform = platform; this.accessory = accessory; this.config = platform.config; this.OPPO_IP = this.config.ip; this.IPReceived = false; this.localIP = '192.168.0.2' this.statelessTimeOut = 1000; //////Initial Switch and sensors state/////////////////////////////////////////////////////////////////////////////////////////////// this.powerState = false; this.playBackState = [false, false, false]; this.inputState = [false, false, false, false, false, false, false, false, false, false]; this.HDROutput = [false, false, false]; this.audioType = [false, false]; this.powerStateTV = 0; this.currentVolume = 0; this.targetVolume = 100; this.currentTime = 0; this.currentMuteState = true; this.currentVolumeSwitch = false; this.inputID = 1; this.mediaState = 4; this.loginTimeOut = true; this.netConnectTimeOut = true; this.turnOffAllUsed = false; this.videoState = false; this.audioState = false; this.inputName = 'Blu-ray'; this.mediaDuration = 'Runtime'; this.mediaChapter = 'Current Chapter'; this.mediaAudioFormat = 'Video and Audio Format'; this.language = 'Audio Language'; this.latestAudioType = ''; this.latestAudioName = ''; this.newSubtitle = ''; this.showState = false; this.firstHttp = true; this.continueSendingUpdate = true this.key = this.query('VERBOSE MODE'); this.httpNotResponding = 0; this.turnOffCommandOn = false; this.turnOnCommandOn = false; /////MovieConstants this.currentMovieProgress = 0; this.currentChapterSelector = [0, 0]; this.currentChapterTime = 0; this.currentChapterTimeState = false; this.currentChapterSelectorState = false; this.currentMovieProgressState = false; this.movieRemaining = 0; this.firstElapsedMovie = 0; this.chapterRemaining = 0; this.currentMovieProgressFirst = true; this.chapterFirstUpdate = true; this.chapterFirstUpdateRemaining = true; this.chapterRemainingFirst = 0; this.chapterElapsedFirst = 0; this.chapterCounter = 0; this.chapterUpdateSec = 60; this.movieType = ''; this.diskType = '' this.chapterProgressUpdate = true; this.chapterNumberRequest = true; ////Connection parameters this.reconnectionCounter = 0; this.reconnectionTry = 10; this.connectionLimit = false; this.connectionLimitStatus = 0; this.mediaDetailsCounter = 0; this.reconnectionWait = platform.config.pollingInterval || 10000; this.firstConnection = false; this.reconnectTimer = null; this.hourlyReconnectTimer = null; this.connectionInterval = null; this.syncInterval = null; this.historyInterval = null; this.continueSending = true; this.newResponse = ''; this.videoIn3D = ''; this.loginCounter = 0; this.mediaHoursOrMinutes = ''; this.chapterHoursOrMinutes = ''; this.lastQuerySentAt = {}; this.lastCommandSentAt = {}; this.scheduledTimers = {}; this.historyDirectoryReady = false; this.historyDirectoryPath = ''; this.commandNameCache = new Map(); this.debugLogWindows = {}; this.metrics = { tcpWrites: 0, httpRequests: 0, skippedQueries: 0, coalescedCommands: 0, lastMetricsLogAt: Date.now(), }; //Device Information////////////////////////////////////////////////////////////////////////////////////// this.applyConfigDefaults(platform.config); this.validateRuntimeConfig(); if (this.config.autoIP === true) { this.config.autoIP = false; } else { this.config.autoIP = true; } this.reconnectionWait = this.config.pollingInterval || 10000; ////Checking if the necessary information was given by the user//////////////////////////////////////////////////// try { if (!this.OPPO_IP && this.config.autoIP === false) { throw new Error(`Oppo IP address is required for ${this.config.name}`); } } catch (error) { this.platform.log(error); this.platform.log('Failed to create platform device, missing mandatory information!'); this.platform.log('Please check your device config!'); return; } // set accessory information////////////////////////////////////////////////////////////////////////////////////////// this.accessory.getService(this.platform.Service.AccessoryInformation) .setCharacteristic(this.platform.Characteristic.Manufacturer, this.config.manufacture) .setCharacteristic(this.platform.Characteristic.Model, this.config.modelName) .setCharacteristic(this.platform.Characteristic.SerialNumber, this.config.serialN) .setCharacteristic(this.platform.Characteristic.FirmwareRevision, '5.1.0'); /////////Television Controls/////////////////////////////////////////////////////////////////////////////////////////// // add the tv service this.tvService = this.accessory.getService(this.config.name) || this.accessory.addService(this.platform.Service.Television, this.config.name, SERVICE_IDS.TV_MAIN); this.tvService.setCharacteristic(this.platform.Characteristic.ConfiguredName, this.config.name); this.tvService.setCharacteristic(this.platform .Characteristic.SleepDiscoveryMode, this.platform.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE); this.tvService.getCharacteristic(this.platform.Characteristic.Active) .on('set', (newValue, callback) => { this.platform.log.debug('Set Oppo Active to: ' + newValue); if (newValue === 1) { this.turnOffCommandOn = false; this.turnOnCommandOn = true; ///this.platform.log('Hello33');//////////////// this.sending([this.pressedButton('POWER ON')]); if (this.turnOnCommandOn === true) { setTimeout(() => { this.turnOnCommandOn = false; }, 3000); } } else { this.turnOnCommandOn = false; this.turnOffCommandOn = true; if (this.playBackState[0] === true || this.playBackState[1] === true) { this.sending([this.pressedButton('STOP')]); setTimeout(() => { this.turnOffAll(); this.sending([this.pressedButton('POWER OFF')]); if (this.turnOffCommandOn === true) { setTimeout(() => { this.turnOffCommandOn = false; }, 3000); } }, 1000); } else { this.turnOffAll(); this.sending([this.pressedButton('POWER OFF')]); if (this.turnOffCommandOn === true) { setTimeout(() => { this.turnOffCommandOn = false; }, 3000); } } } callback(null); }) .on('get', (callback) => { let currentValue = this.powerStateTV; callback(null, currentValue); }); this.tvService.getCharacteristic(this.platform.Characteristic.ClosedCaptions) .on('get', (callback) => { this.platform.log.debug('Subtitle GET On'); let currentValue = 0; callback(null, currentValue); }) .on('set', (value, callback) => { this.platform.log.debug('Subtitle SET On:', value); if (value === 1) { this.sending([this.pressedButton('SUBTITLE')]); } this.tvService.updateCharacteristic(this.platform.Characteristic.ClosedCaptions, 0); callback(null); }); //////Things to remove this.tvService.getCharacteristic(this.platform.Characteristic.Brightness) .on('get', (callback) => { let currentValue = this.currentVolume; callback(null, currentValue); }) .on('set', (newValue, callback) => { this.sending([this.volumeChange(newValue)]); this.platform.log.debug('Volume Value set to: ' + newValue); callback(null); }); this.tvService.getCharacteristic(this.platform.Characteristic.PictureMode) .on('set', (newValue, callback) => { if (newValue === 1) { this.sending([this.pressedButton('VOLUME DOWN')]); } if (newValue === 0) { this.sending([this.pressedButton('VOLUME UP')]); } this.platform.log('Volume Value moved by: ' + newValue); callback(null); }); //////////////// this.tvService.getCharacteristic(this.platform.Characteristic.RemoteKey) .on('set', (newValue, callback) => { switch (newValue) { case this.platform.Characteristic.RemoteKey.REWIND: { this.platform.log.debug('set Remote Key Pressed: REWIND'); this.sending([this.pressedButton('REWIND')]); break; } case this.platform.Characteristic.RemoteKey.FAST_FORWARD: { this.platform.log.debug('set Remote Key Pressed: FAST_FORWARD'); this.sending([this.pressedButton('FORWARD')]); break; } case this.platform.Characteristic.RemoteKey.NEXT_TRACK: { this.platform.log.debug('set Remote Key Pressed: NEXT_TRACK'); this.sending([this.pressedButton('NEXT')]); break; } case this.platform.Characteristic.RemoteKey.PREVIOUS_TRACK: { this.platform.log.debug('set Remote Key Pressed: PREVIOUS_TRACK'); this.sending([this.pressedButton('PREVIOUS')]); break; } case this.platform.Characteristic.RemoteKey.ARROW_UP: { this.platform.log.debug('set Remote Key Pressed: ARROW_UP'); this.sending([this.pressedButton('CURSOR UP')]); break; } case this.platform.Characteristic.RemoteKey.ARROW_DOWN: { this.platform.log.debug('set Remote Key Pressed: ARROW_DOWN'); this.sending([this.pressedButton('CURSOR DOWN')]); break; } case this.platform.Characteristic.RemoteKey.ARROW_LEFT: { this.platform.log.debug('set Remote Key Pressed: ARROW_LEFT'); this.sending([this.pressedButton('CURSOR LEFT')]); break; } case this.platform.Characteristic.RemoteKey.ARROW_RIGHT: { this.platform.log.debug('set Remote Key Pressed: ARROW_RIGHT'); this.sending([this.pressedButton('CURSOR RIGHT')]); break; } case this.platform.Characteristic.RemoteKey.SELECT: { this.platform.log.debug('set Remote Key Pressed: SELECT'); this.sending([this.pressedButton('CURSOR ENTER')]); break; } case this.platform.Characteristic.RemoteKey.BACK: { this.platform.log.debug('set Remote Key Pressed: BACK'); this.sending([this.pressedButton('BACK')]); break; } case this.platform.Characteristic.RemoteKey.EXIT: { this.platform.log.debug('set Remote Key Pressed: EXIT'); this.sending([this.pressedButton('HOME MENU')]); break; } case this.platform.Characteristic.RemoteKey.PLAY_PAUSE: { this.platform.log.debug('set Remote Key Pressed: PLAY_PAUSE'); if (this.playBackState[0] === false) { this.sending([this.pressedButton('PLAY')]); } else { this.sending([this.pressedButton('PAUSE')]); } break; } case this.platform.Characteristic.RemoteKey.INFORMATION: { if (this.config.infoToMenu) { if (this.movieType === 'C') { this.platform.log.debug('set Remote Key Pressed: OPTION'); this.sending([this.pressedButton('OPTION')]); } else { this.platform.log.debug('set Remote Key Pressed: MENU'); this.sending([this.pressedButton('POP-UP MENU')]); } } else { this.platform.log.debug('set Remote Key Pressed: INFORMATION'); this.sending([this.pressedButton('INFO')]); } break; } } callback(null); }); //////////////////////////////////TV Service////////////////////////////////////////////////////////////////////////// this.tvService .setCharacteristic(this.platform.Characteristic.ActiveIdentifier, this.inputID); this.tvService .getCharacteristic(this.platform.Characteristic.ActiveIdentifier) .on('set', (inputIdentifier, callback) => { this.platform.log.debug('Active Identifier set to:', inputIdentifier); if (inputIdentifier === 999999) { this.newInputState([false, false, false, false, false, false, false, false, false, false]); this.inputID = 1; } if (inputIdentifier === 0) { this.inputID = 1; this.newInputState([false, false, false, false, false, false, false, false, false, false]); } else if (inputIdentifier === 1) { this.inputID = inputIdentifier; this.sending([this.pressedButton('BLURAY INPUT')]); } else if (inputIdentifier === 2) { this.inputID = inputIdentifier; } else if (inputIdentifier === 3) { this.inputID = inputIdentifier; } else if (inputIdentifier === 4) { this.inputID = inputIdentifier; } else if (inputIdentifier === 5) { this.inputID = inputIdentifier; } else if (inputIdentifier === 6) { this.inputID = inputIdentifier; this.sending([this.pressedButton('HDMI IN')]); } else if (inputIdentifier === 7) { this.inputID = inputIdentifier; this.sending([this.pressedButton('HDMI OUT')]); } else if (inputIdentifier === 8) { this.inputID = inputIdentifier; this.sending([this.pressedButton('OPTICAL INPUT')]); } else if (inputIdentifier === 9) { this.inputID = inputIdentifier; this.sending([this.pressedButton('COAXIAL INPUT')]); } else if (inputIdentifier === 10) { this.inputID = inputIdentifier; this.sending([this.pressedButton('USB AUDIO INPUT')]); } else { this.inputID = 1; } callback(); }) .on('get', (callback) => { let currentValue = this.inputID; this.platform.log.debug('Active Identifier set to:', currentValue); callback(null, currentValue); }); this.tvService .getCharacteristic(this.platform.Characteristic.PowerModeSelection) .on('set', (newValue, callback) => { this.platform.log.debug('Requested Oppo Settings ' + newValue); if (this.playBackState[0] === false && this.playBackState[1] === false && this.playBackState[2] === false) { this.sending([this.pressedButton('SETUP')]); } else { this.sending([this.pressedButton('POP-UP MENU')]); } callback(); }); // Input Sources/////////////////////////////////////////////////////////////////////////////////////////////////////////// this.buildInputSources(); /////Media State///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState) .on('get', (callback) => { let currentValue = this.mediaState; this.platform.log.debug('Current Playback State', currentValue); callback(null, currentValue); }); this.tvService.getCharacteristic(this.platform.Characteristic.TargetMediaState) .on('get', (callback) => { let currentValue = this.mediaState; if (this.mediaState === 4) { currentValue = 2; } this.platform.log.debug('Current Playback State', currentValue); callback(null, currentValue); }) .on('set', (value, callback) => { if (value === 0) { this.sending([this.pressedButton('PLAY')]); } else if (value === 1) { this.sending([this.pressedButton('PAUSE')]); } else if (value === 2) { this.sending([this.pressedButton('STOP')]); } this.platform.log.debug('Playback State set to:', value); callback(null); }); this.buildSpeakerVolumeService(); this.buildVideoMovieControls(); this.buildAdditionalServices(accessory.context.device.oppoDisplayName); ////other Controls ///////////////////////////////////////////////////////// this.setupStatelessSwitches(); //Movie Timer if (this.config.remainMovieTimer) { this.movieTimer = accessory.getService(this.platform.Service.Valve) || accessory.addService(this.platform.Service.Valve, 'Oppo Movie Timer', 'Movie Timer'); this.movieTimer.setCharacteristic(this.platform.Characteristic.Name, 'Movie Timer'); this.movieTimer.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName); this.movieTimer.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Oppo Movie Timer'); this.movieTimer.setCharacteristic(this.platform.Characteristic.ValveType, this.platform.Characteristic.ValveType.IRRIGATION); this.movieTimer.getCharacteristic(this.platform.Characteristic.Active) .on('get', (callback) => { let currentValue = this.currentMovieProgressState ? 1 : 0 callback(null, currentValue); }) .on('set', (value, callback) => { callback(null); }); this.movieTimer.setCharacteristic(this.platform.Characteristic.InUse, this.platform.Characteristic.InUse.NOT_IN_USE); this.movieTimer.getCharacteristic(this.platform.Characteristic.RemainingDuration) .on('get', (callback) => { let currentValue = this.movieRemaining; callback(null, currentValue); }) .setProps({ maxValue: 86400 / 4, // 1 day }); this.movieTimer.getCharacteristic(this.platform.Characteristic.SetDuration) .on('get', (callback) => { let currentValue = this.firstElapsedMovie + this.movieRemaining; callback(null, currentValue); }) .setProps({ maxValue: 86400 / 4, // 1 day }); } this.initCharacteristicCache(); ///////////////Clean up. Delete services not in used this.cleanupServices(); //////////////////Connecting to Oppo if (this.config.autoIP === true) { this.discoveryUDP(); } else { this.login(); setTimeout(() => { this.netCo