From bad01f6ef4c01757710e836d395af9df4367040e Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Sun, 8 Jan 2017 22:24:52 +0100 Subject: [PATCH] initial commit --- bravia.js | 59 ++++++++++++++++++ cmds.json | 1 + netflixkb.js | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 +++++ tasks.js | 9 +++ utils.js | 7 +++ 6 files changed, 261 insertions(+) create mode 100644 bravia.js create mode 100644 cmds.json create mode 100644 netflixkb.js create mode 100644 package.json create mode 100644 tasks.js create mode 100644 utils.js diff --git a/bravia.js b/bravia.js new file mode 100644 index 0000000..8cb8ce9 --- /dev/null +++ b/bravia.js @@ -0,0 +1,59 @@ +const rp = require('request-promise') +const Promise = require('bluebird') +const utils = require('./utils') +class Bravia { + constructor(ip, secret) { + Object.assign(this, {ip, secret, cmd_cache: {}}) + } + request(options) { + options.url = `http://${this.ip}${options.path}` + options.headers = options.headers || {} + options.headers['X-Auth-PSK'] = this.secret + return rp.post(options) + } + sendIRCC(ircc) { + const body = ` + + + ${ircc} + + ` + return this.request({ + path: '/sony/IRCC', body, headers: { + 'Content-Type': 'text/xml charset=UTF-8', + 'SOAPACTION': '"urn:schemas-sony-com:service:IRCC:1#X_SendIRCC"' + } + }) + } + sendRPC(endpoint, method, ...params) { + // endpoint \in {system, accessControl, encryption, recording, browser, appControl} + // "getMethodTypes", "" is a good one to try + return this.request({ + path: `/sony/${endpoint}`, + json: true, + body: { method, params, id: 1, version: "1.0" } + }) + } + lookupCommand(cmd, do_lookup = true) { + if (this.cmd_cache[cmd]) return Promise.resolve(this.cmd_cache[cmd]) + if(do_lookup) return this.sendRPC("system", "getRemoteControllerInfo").then((body) => { + body.result[1].forEach(({name, value}) => this.cmd_cache[name] = value) + return this.lookupCommand(cmd, false) + }) + else return Promise.reject("command not found") + } + sendCommand(cmd) { + console.log("sending", cmd) + return typeof cmd === 'number' ? Promise.delay(cmd) : + Array.isArray(cmd) ? this.sequence(cmd) : + this.lookupCommand(cmd).then(ircc => this.sendIRCC(ircc)) + } + sequence([cmd, ...commands]) { + if (cmd == undefined) return Promise.resolve() + return this.sendCommand(cmd).then(() => this.sequence(commands)) + } + delaySequence(cmds, delay=250) { + this.sequence(utils.alternate(cmds, 250)) + } +} +module.exports = Bravia diff --git a/cmds.json b/cmds.json new file mode 100644 index 0000000..a0005e8 --- /dev/null +++ b/cmds.json @@ -0,0 +1 @@ +{"result":[{"bundled":true,"type":"IR_REMOTE_BUNDLE_TYPE_AEP_N"},[{"name":"Num1","value":"AAAAAQAAAAEAAAAAAw=="},{"name":"Num2","value":"AAAAAQAAAAEAAAABAw=="},{"name":"Num3","value":"AAAAAQAAAAEAAAACAw=="},{"name":"Num4","value":"AAAAAQAAAAEAAAADAw=="},{"name":"Num5","value":"AAAAAQAAAAEAAAAEAw=="},{"name":"Num6","value":"AAAAAQAAAAEAAAAFAw=="},{"name":"Num7","value":"AAAAAQAAAAEAAAAGAw=="},{"name":"Num8","value":"AAAAAQAAAAEAAAAHAw=="},{"name":"Num9","value":"AAAAAQAAAAEAAAAIAw=="},{"name":"Num0","value":"AAAAAQAAAAEAAAAJAw=="},{"name":"Num11","value":"AAAAAQAAAAEAAAAKAw=="},{"name":"Num12","value":"AAAAAQAAAAEAAAALAw=="},{"name":"Enter","value":"AAAAAQAAAAEAAAALAw=="},{"name":"GGuide","value":"AAAAAQAAAAEAAAAOAw=="},{"name":"ChannelUp","value":"AAAAAQAAAAEAAAAQAw=="},{"name":"ChannelDown","value":"AAAAAQAAAAEAAAARAw=="},{"name":"VolumeUp","value":"AAAAAQAAAAEAAAASAw=="},{"name":"VolumeDown","value":"AAAAAQAAAAEAAAATAw=="},{"name":"Mute","value":"AAAAAQAAAAEAAAAUAw=="},{"name":"TvPower","value":"AAAAAQAAAAEAAAAVAw=="},{"name":"Audio","value":"AAAAAQAAAAEAAAAXAw=="},{"name":"MediaAudioTrack","value":"AAAAAQAAAAEAAAAXAw=="},{"name":"Tv","value":"AAAAAQAAAAEAAAAkAw=="},{"name":"Input","value":"AAAAAQAAAAEAAAAlAw=="},{"name":"TvInput","value":"AAAAAQAAAAEAAAAlAw=="},{"name":"TvAntennaCable","value":"AAAAAQAAAAEAAAAqAw=="},{"name":"WakeUp","value":"AAAAAQAAAAEAAAAuAw=="},{"name":"PowerOff","value":"AAAAAQAAAAEAAAAvAw=="},{"name":"Sleep","value":"AAAAAQAAAAEAAAAvAw=="},{"name":"Right","value":"AAAAAQAAAAEAAAAzAw=="},{"name":"Left","value":"AAAAAQAAAAEAAAA0Aw=="},{"name":"SleepTimer","value":"AAAAAQAAAAEAAAA2Aw=="},{"name":"Analog2","value":"AAAAAQAAAAEAAAA4Aw=="},{"name":"TvAnalog","value":"AAAAAQAAAAEAAAA4Aw=="},{"name":"Display","value":"AAAAAQAAAAEAAAA6Aw=="},{"name":"Jump","value":"AAAAAQAAAAEAAAA7Aw=="},{"name":"PicOff","value":"AAAAAQAAAAEAAAA+Aw=="},{"name":"PictureOff","value":"AAAAAQAAAAEAAAA+Aw=="},{"name":"Teletext","value":"AAAAAQAAAAEAAAA\/Aw=="},{"name":"Video1","value":"AAAAAQAAAAEAAABAAw=="},{"name":"Video2","value":"AAAAAQAAAAEAAABBAw=="},{"name":"AnalogRgb1","value":"AAAAAQAAAAEAAABDAw=="},{"name":"Home","value":"AAAAAQAAAAEAAABgAw=="},{"name":"Exit","value":"AAAAAQAAAAEAAABjAw=="},{"name":"PictureMode","value":"AAAAAQAAAAEAAABkAw=="},{"name":"Confirm","value":"AAAAAQAAAAEAAABlAw=="},{"name":"Up","value":"AAAAAQAAAAEAAAB0Aw=="},{"name":"Down","value":"AAAAAQAAAAEAAAB1Aw=="},{"name":"ClosedCaption","value":"AAAAAgAAAKQAAAAQAw=="},{"name":"Component1","value":"AAAAAgAAAKQAAAA2Aw=="},{"name":"Component2","value":"AAAAAgAAAKQAAAA3Aw=="},{"name":"Wide","value":"AAAAAgAAAKQAAAA9Aw=="},{"name":"EPG","value":"AAAAAgAAAKQAAABbAw=="},{"name":"PAP","value":"AAAAAgAAAKQAAAB3Aw=="},{"name":"TenKey","value":"AAAAAgAAAJcAAAAMAw=="},{"name":"BSCS","value":"AAAAAgAAAJcAAAAQAw=="},{"name":"Ddata","value":"AAAAAgAAAJcAAAAVAw=="},{"name":"Stop","value":"AAAAAgAAAJcAAAAYAw=="},{"name":"Pause","value":"AAAAAgAAAJcAAAAZAw=="},{"name":"Play","value":"AAAAAgAAAJcAAAAaAw=="},{"name":"Rewind","value":"AAAAAgAAAJcAAAAbAw=="},{"name":"Forward","value":"AAAAAgAAAJcAAAAcAw=="},{"name":"DOT","value":"AAAAAgAAAJcAAAAdAw=="},{"name":"Rec","value":"AAAAAgAAAJcAAAAgAw=="},{"name":"Return","value":"AAAAAgAAAJcAAAAjAw=="},{"name":"Blue","value":"AAAAAgAAAJcAAAAkAw=="},{"name":"Red","value":"AAAAAgAAAJcAAAAlAw=="},{"name":"Green","value":"AAAAAgAAAJcAAAAmAw=="},{"name":"Yellow","value":"AAAAAgAAAJcAAAAnAw=="},{"name":"SubTitle","value":"AAAAAgAAAJcAAAAoAw=="},{"name":"CS","value":"AAAAAgAAAJcAAAArAw=="},{"name":"BS","value":"AAAAAgAAAJcAAAAsAw=="},{"name":"Digital","value":"AAAAAgAAAJcAAAAyAw=="},{"name":"Options","value":"AAAAAgAAAJcAAAA2Aw=="},{"name":"Media","value":"AAAAAgAAAJcAAAA4Aw=="},{"name":"Prev","value":"AAAAAgAAAJcAAAA8Aw=="},{"name":"Next","value":"AAAAAgAAAJcAAAA9Aw=="},{"name":"DpadCenter","value":"AAAAAgAAAJcAAABKAw=="},{"name":"CursorUp","value":"AAAAAgAAAJcAAABPAw=="},{"name":"CursorDown","value":"AAAAAgAAAJcAAABQAw=="},{"name":"CursorLeft","value":"AAAAAgAAAJcAAABNAw=="},{"name":"CursorRight","value":"AAAAAgAAAJcAAABOAw=="},{"name":"ShopRemoteControlForcedDynamic","value":"AAAAAgAAAJcAAABqAw=="},{"name":"FlashPlus","value":"AAAAAgAAAJcAAAB4Aw=="},{"name":"FlashMinus","value":"AAAAAgAAAJcAAAB5Aw=="},{"name":"AudioQualityMode","value":"AAAAAgAAAJcAAAB7Aw=="},{"name":"DemoMode","value":"AAAAAgAAAJcAAAB8Aw=="},{"name":"Analog","value":"AAAAAgAAAHcAAAANAw=="},{"name":"Mode3D","value":"AAAAAgAAAHcAAABNAw=="},{"name":"DigitalToggle","value":"AAAAAgAAAHcAAABSAw=="},{"name":"DemoSurround","value":"AAAAAgAAAHcAAAB7Aw=="},{"name":"*AD","value":"AAAAAgAAABoAAAA7Aw=="},{"name":"AudioMixUp","value":"AAAAAgAAABoAAAA8Aw=="},{"name":"AudioMixDown","value":"AAAAAgAAABoAAAA9Aw=="},{"name":"PhotoFrame","value":"AAAAAgAAABoAAABVAw=="},{"name":"Tv_Radio","value":"AAAAAgAAABoAAABXAw=="},{"name":"SyncMenu","value":"AAAAAgAAABoAAABYAw=="},{"name":"Hdmi1","value":"AAAAAgAAABoAAABaAw=="},{"name":"Hdmi2","value":"AAAAAgAAABoAAABbAw=="},{"name":"Hdmi3","value":"AAAAAgAAABoAAABcAw=="},{"name":"Hdmi4","value":"AAAAAgAAABoAAABdAw=="},{"name":"TopMenu","value":"AAAAAgAAABoAAABgAw=="},{"name":"PopUpMenu","value":"AAAAAgAAABoAAABhAw=="},{"name":"OneTouchTimeRec","value":"AAAAAgAAABoAAABkAw=="},{"name":"OneTouchView","value":"AAAAAgAAABoAAABlAw=="},{"name":"DUX","value":"AAAAAgAAABoAAABzAw=="},{"name":"FootballMode","value":"AAAAAgAAABoAAAB2Aw=="},{"name":"iManual","value":"AAAAAgAAABoAAAB7Aw=="},{"name":"Netflix","value":"AAAAAgAAABoAAAB8Aw=="},{"name":"Assists","value":"AAAAAgAAAMQAAAA7Aw=="},{"name":"ActionMenu","value":"AAAAAgAAAMQAAABLAw=="},{"name":"Help","value":"AAAAAgAAAMQAAABNAw=="},{"name":"TvSatellite","value":"AAAAAgAAAMQAAABOAw=="},{"name":"WirelessSubwoofer","value":"AAAAAgAAAMQAAAB+Aw=="}]],"id":20} diff --git a/netflixkb.js b/netflixkb.js new file mode 100644 index 0000000..c7bd6f5 --- /dev/null +++ b/netflixkb.js @@ -0,0 +1,170 @@ +// Q: why would you do this +// A: the password on the netflix account I use is long +// and it doesn't have proper text input support +// Q: how do I use this +// A: bravia.delaySequence(require('netflixkb.js').NetflixPW.sequence("yourpassword")) +"use strict" +const FibonacciHeap = require('@tyriar/fibonacci-heap') +// priority-queue dijkstra, reference https://en.wikipedia.org/wiki/Dijkstra's_algorithm +function find_path(target, prev) { + const S = [] + let u = {node: target} + while(prev.has(u.node)) { + u = prev.get(u.node) + S.push(u) + } + return [S.reverse().map(x => x.label), S[S.length - 1].node] +} +function dijkstra(source, target) { + const dist = new Map() + const prev = new Map() + const heapnodes = new Map() + const Q = new FibonacciHeap() + dist.set(source, 0) + heapnodes.set(source, Q.insert(0, source)) + while(!Q.isEmpty()) { + const {value: u} = Q.extractMinimum() + heapnodes.delete(u) + if (u == target) { + return find_path(target, prev) + } + if (u.transitions) { + for(const [cost, v, label] of u.transitions()) { + const alt = dist.get(u) + cost + if (alt < (dist.has(v) ? dist.get(v) : Infinity)) { + dist.set(v, alt) + prev.set(v, Object.freeze({node: u, label})) + if (heapnodes.has(v)) Q.decreaseKey(heapnodes.get(v), alt) + else heapnodes.set(v, Q.insert(alt, v)) + } + } + } + } + return undefined +} + +// TODO: find better solution for the special keys +// wraps to left and right +// \x1: shift. \x2: symbols, \x11: backspace +// 3: diacritics \x05: uppercase symbols, \x06: uppercase diacritics +// \x10: too lazy, some sort of superscript a +const netflixpw_kb = Object.freeze([[ + "1234567890", + "qwertyuiop", + "asdfghjkl-", + "\x01\x01zxcvbnm'", + "\x02\x02\x03\x03 \x11\x11\x11" + ],[ + "1234567890", + "QWERTYUIOP", + "ASDFGHJKL-", + "\x00\x00ZXCVBNM'", + "\x05\x05\x06\x06 \x11\x11\x11" + ],[ + "`~!@#$%^&*", + "()-_=+[]{}", + "\\|;:'\",.<>", + "/?¡¿\x10°¢€£¥", + "\x00\x00\x03\x03 \x11\x11\x11" + ] +]) +// 0: standard. 1: shift. 2: special chars. 3: uppercase special chars +// 11: backspace. 12: @gmail.com. 13: @hotmail.com. 14: @live.nl. 15: .com. +// 16: .ca 17 .net 18 .edu 19 .org 1a .gov +const netflixemail = Object.freeze([[ + "1234567890", + "qwertyuiop", + "asdfghjkl-", + "\x01\x01zxcvbnm_", + "\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14", + "\x02\x02@@.\x15\x15\x11\x11\x11", + ],[ + "1234567890", + "QWERTYUIOP", + "ASDFGHJKL-", + "\x00\x00ZXCVBNM_", + "\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14", + "\x03\x03@@.\x15\x15\x11\x11\x11", + ],[ + "1234567890", + "`~!@#$%^&*", + "+-_{}|'./?", + "\x16\x16\x17\x17\x18\x18\x19\x19\x1a\x1a", + "\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14", + "\x00\x00@@.\x15\x15\x11\x11\x11", + ] +]) +const netflixsearch = Object.freeze([[ + ' \x11\x11\x11', + 'abcdef', + 'ghijkl', + 'mnopqr', + 'stuvwx', + 'yz1234', + '567890' +]]) +class Keyboard { + constructor(chars, startpos, wraparound = true, centeroffs = true) { + Object.assign(this, { + chars, depth: chars.length, height: chars[0].length, width: chars[0][0].length, + startpos, wraparound, centeroffs // workaround because the search keyboard is different + }) + this.nodes = chars.map((kb, kbid) => + kb.map((row, rowid) => + row.split('').map((letter, colid) => + this.mkNode(kbid, rowid, colid)))) + } + findGroupInfo(kb, row, col) { + const kbrow = this.chars[kb][row] + const idxs = kbrow.split('').map((ltr,id) => ltr == kbrow[col] ? id : -1).filter(id => id>=0) + return Object.freeze({left: idxs[0], size: idxs.length}) + } + findGroupCenter(kb, row, col) { + col = (col + this.width) % this.width // fix wraparound + const {left, size} = this.findGroupInfo(kb, row, col) + if (!this.centeroffs) return this.nodes[kb][row][left] + return this.nodes[kb][row][left + Math.ceil(size / 2) - 1] + } + mkNode(kb, row, col) { + return Object.freeze({ + letter: this.chars[kb][row][col], kb, row, col, + transitions: () => { + const {left, size} = this.findGroupInfo(kb, row, col) + const transitions = { + 'Left': this.findGroupCenter(kb, row, left - 1), + 'Right': this.findGroupCenter(kb, row, left + size) + } + if (!this.wraparound && col == 0) delete transitions['Left'] + if (!this.wraparound && col == this.width-1) delete transitions['Right'] + if (row != 0) transitions['Up'] = this.nodes[kb][row-1][col] + if (row != this.height-1) transitions['Down'] = this.nodes[kb][row+1][col] + { + const char = this.chars[kb][row][col] + const cc = char.charCodeAt(0) + if (cc < this.depth) transitions['DpadCenter'] = this.findGroupCenter(cc, row, col) + else if (cc > 15) transitions['DpadCenter'] = char + } + return Object.keys(transitions).map(tname => [1, transitions[tname], tname]) + } + }) + } + lookupNode(z, y, x) { return this.nodes[z][y][x] } + sequence(string, goback = false, start = undefined) { + if (!start) start = this.lookupNode(...this.startpos) + let pos = start + const seq = [] + for(const char of string) { + let s2; + [s2, pos] = dijkstra(pos, char) + seq.push(s2) + } + if (goback) return [[].concat(...seq, dijkstra(pos, start)[0]), start] + return [[].concat(...seq), pos] + } +} +module.exports = { + Keyboard, + NetflixPW: new Keyboard(netflixpw_kb, [0,2,4]), + NetflixMail: new Keyboard(netflixemail, [0,2,4]), + NetflixSearch: new Keyboard(netflixsearch, [0, 1, 0], false, false) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0efbc6 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "bravia-remote", + "version": "1.0.0", + "description": "", + "main": "bravia.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@tyriar/fibonacci-heap": "^1.0.10", + "request-promise": "^4.1.1" + } +} diff --git a/tasks.js b/tasks.js new file mode 100644 index 0000000..70da91f --- /dev/null +++ b/tasks.js @@ -0,0 +1,9 @@ +const {repeat} = require('./utils') +const speakermenu = [ + 'WakeUp', 2000, 'Exit', 300, 'ActionMenu', 1000, repeat(['Up', 250], 9), repeat(['Down', 250], 7), 'DpadCenter', 1000 +] +const speakermode = [speakermenu, 'Down', 500, 'DpadCenter', 500] + +const unspeakermode = [speakermenu, 'Up', 500, 'DpadCenter', 500] +const hdmi2audio = ['WakeUp', 5000, 'Hdmi2', 1000, speakermode, 'PicOff'] +Object.assign(module.exports, {speakermenu, speakermode, unspeakermode, hdmi2audio}) diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..c0fa78e --- /dev/null +++ b/utils.js @@ -0,0 +1,7 @@ +function *repeat(seq, times) { + for(let i = 0; i < times; i++) yield *seq +} +function *alternate(a, delim) { + for(const x of a) { yield *[x, delim] } +} +Object.assign(module.exports, {repeat, alternate})