homebridge-oppo-udp
Version:
Homebridge Plugin to control your Oppo 203/205.
1,022 lines (1,015 loc) • 198 kB
JavaScript
"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