├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── config.schema.json ├── index.js └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: normen 2 | patreon: normen 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .homebridge/ 3 | package-lock.json 4 | tags 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for homebridge-bravia 2 | This is the change log for the plugin, all relevant changes will be listed here. 3 | 4 | For documentation please see the [README](https://github.com/normen/homebridge-bravia/blob/master/README.md) 5 | 6 | ## 2.4.9 7 | - Use proper storage folder `plugin-persist/homebridge-bravia` 8 | 9 | ## 2.4.8 10 | - Avoid annoying warnings 11 | 12 | ## 2.4.7 13 | - Small improvements 14 | 15 | ## 2.4.6 16 | - Store channels for external TVs in file as the device cache in homebridge doesn't store external devices 17 | - Improve channel update logic 18 | 19 | ## 2.4.5 20 | - Allow enabling debug output per TV through UI 21 | - Fix debug output not working 22 | 23 | ## 2.4.4 24 | - Fix UUID issue causing external TVs to be added twice 25 | - External TVs might have to be added again 26 | 27 | ## 2.4.3 28 | - Fix adding devices in non-external mode 29 | 30 | ## 2.4.2 31 | - Fix external accessory mode (not enabled by default) 32 | - External accessory mode will currently need to re-scan the TV channels on each homebridge boot 33 | 34 | ## 2.4.1 35 | - disable external accessory mode as its broken as of now 36 | 37 | ## 2.4 38 | - make externalaccessory mode the default 39 | 40 | ## 2.3.2 41 | - add accessory category to fix icon display 42 | 43 | ## 2.3.1 44 | - fix removal of nonexisting accessories 45 | - update README with new options 46 | 47 | ## 2.3 48 | - add option to register TV as external accessory, 49 | this allows multiple TVs to appear in the remote app 50 | 51 | ## 2.2.3 52 | - hide more warnings 53 | 54 | ## 2.2.2 55 | - README updates 56 | - only log warnings when in debug mode 57 | - improve error checking of TV responses 58 | 59 | ## 2.2.1 60 | - layout fix 61 | 62 | ## 2.2.0 63 | - cleanups 64 | - add changelog 65 | - add development info 66 | - fix client ID issue for waking up TV 67 | 68 | ## 2.1.11 69 | - fix error when plugin is started without config (old homebridge) 70 | 71 | ## 2.1.10 72 | - only scan channels if TV is on 73 | 74 | ## 2.1.9 75 | - increase security by using unique uuid per instance 76 | 77 | ## 2.1.8 78 | - fix channelupdaterate 79 | 80 | ## 2.1.7 81 | - allow renaming channels 82 | 83 | ## 2.1.6 84 | - improve internal channel number handling 85 | 86 | ## 2.1.5 87 | - use map for channel identifier 88 | 89 | ## 2.1.4 90 | - avoid escalating channel identifiers 91 | 92 | ## 2.1.3 93 | - allow setting address for WOL 94 | 95 | ## 2.1.2 96 | - cleanups 97 | - small fixes 98 | - less scary error messages 99 | 100 | ## 2.1.1 101 | - update channels continuously by default 102 | 103 | ## 2.1.0 104 | - allow updates of channel/app list 105 | 106 | ## 2.0.4 107 | - fix error when channels appear twice in the TV 108 | 109 | ## 2.0.3 110 | - fix crash when channel is not found 111 | 112 | ## 2.0.2 113 | - fix starting applications 114 | 115 | ## 2.0.1 116 | - README updates 117 | - small fix in IR URL 118 | - code cleanups 119 | 120 | ## 2.0.0 121 | - use dynamic accessory model (no more blocking HB boot) 122 | - store cookie file in HB storage path 123 | - store separate cookie files for separate TVs 124 | - use web server for PIN entry 125 | - requires setting up existing TVs again! 126 | 127 | ## 1.3.4 128 | - remove ping test 129 | 130 | ## 1.3.3 131 | - improve config panel 132 | 133 | ## 1.3.2 134 | - fix mac address in config panel 135 | 136 | ## 1.3.1 137 | - add support for config-ui-x settings panels 138 | 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-bravia [](https://www.npmjs.com/package/homebridge-bravia) [](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 2 | 3 | HomeBridge plugin for Sony Bravia TVs (AndroidTV based ones and possibly others). 4 | 5 | ## Introduction 6 | Supports the following functions 7 | - Turning TV on/off 8 | - Setting volume 9 | - Selecting inputs / channels 10 | - Starting apps 11 | - Trigger automation when turning the TV on/off 12 | - iOS 12.2 remote support 13 | - Secure connection to TV without PSK 14 | 15 | This plugin requires iOS 12.2, to use it with previous iOS versions install version 0.96 of this plugin. 16 | 17 | **Note for users of versions before 2.0: Updating to 2.0+ will force you to set up the TV (including all HomeKit automation) again** 18 | 19 | ## Installation 20 | - Install homebridge (e.g. using `npm install -g homebridge`) 21 | - Install this plugin (e.g. using `npm install -g homebridge-bravia`) 22 | - Configure the plugin settings through config.json or web UI (see below for the options) 23 | - Turn on the TV 24 | - Set "Remote start" to ON in your TV Settings->Network->Remote Start (not required) 25 | - Restart Homebridge 26 | - The TV will display a PIN 27 | - Enter the PIN at `http://homebridge.local:8999` 28 | - Replace `homebridge.local` with the IP or name of your homebridge server 29 | - Note that the web server is only accessible when you have to enter a PIN 30 | - Your TV should appear in HomeKit as soon as all channels have been scanned 31 | - For external accessory mode (see below) 32 | - In HomeKit, press the "+" button and select "Add Device" 33 | - Select "I have no code", then enter the code of your homebridge install to add the TV 34 | 35 | ### Configure config.json 36 | Example config: 37 | 38 | ``` 39 | "platforms":[ 40 | { 41 | "platform": "BraviaPlatform", 42 | "tvs": [ 43 | { 44 | "name": "TV", 45 | "ip": "192.168.1.10", 46 | "soundoutput": "speaker", 47 | "tvsource": "tv:dvbs", 48 | "applications": [{"title":"Netflix"}], 49 | "sources": [ 50 | "extInput:hdmi" 51 | ] 52 | } 53 | ] 54 | } 55 | ] 56 | ``` 57 | 58 | Required options: 59 | - `tvs` is the list of Sony TVs in your home 60 | - `name` is the name of your TV as it appears in HomeKit 61 | - `ip` is the IP address or host name of your TV, find and/or set it through your router or set it in the TV 62 | 63 | Optional options (all inside one TV entry): 64 | - `sources` is an array of sources to display in HomeKit 65 | - default `["extInput:hdmi", "extInput:component", "extInput:scart", "extInput:cec", "extInput:widi"]` 66 | - these sources usually represent a type of input, so `extInput:hdmi` will show all your HDMI inputs in HomeKit 67 | - source strings for your TV might look different, check the web if you find the right ones for your TV/input types 68 | - `tvsource` is your preferred TV source, can be `tv:dvbt`, `tv:dvbc` or `tv:dvbs` (antenna, cable or sat), default none 69 | - effectively this is just another source like the ones above 70 | - `applications` can be used to enable listing applications in the input list, default `false` 71 | - Providing an array of objects with application titles will only add applications whose names contain the titles to the input list: 72 | ``` 73 | "applications": [ 74 | { 75 | "title": "Netflix" 76 | }, 77 | { 78 | "title": "Plex" 79 | }, 80 | ... etc. 81 | ] 82 | ``` 83 | - `soundoutput` is your preferred TV sound output, can be `speaker` or `headphone`, default `speaker` 84 | - `port` is the HTTP port of your TV, default 80 85 | - `externalaccessory` if set the TV will be published as an external accessory to HomeKit 86 | - `mac` is the MAC address of your TV, only set it if you want to use WOL instead of HTTP to wake up the TV, default none 87 | - `woladdress` sets the subnet for WOL, default `255.255.255.255` 88 | - `serverPort` sets a different port than `8999` for the web server that allows entering the PIN number from the TV 89 | - `updaterate` interval in milliseconds for TV status updates (on/off etc), default `5000` 90 | - `channelupdaterate` interval in milliseconds for updates of the channel/input list, default `30000` 91 | 92 | ## Usage 93 | ### Basic functions 94 | #### ON/OFF 95 | You can turn your TV on and off through Siri and Apples Home app. 96 | #### Inputs and Applications 97 | All Channels, Inputs and Applications can be selected in the HomeKit inputs selector 98 | #### TV Remote 99 | The TV registers as a TV remote device in HomeKit and allows to use basic function keys and set the volume through the Apple Remote app or iOS configuration screen. Use your phones volume knobs to set the TV volume! 100 | #### TV Speaker 101 | In addition to the iOS remote the plugin also exposes the TV speaker as a HomeKit accessory however only some apps show that accessory type, Apples Home app does not. 102 | 103 | ## Development 104 | If you want new features or improve the plugin, you're very welcome to do so. The projects `devDependencies` include homebridge and the `npm run test` command has been adapted so that you can run a test instance of homebridge during development. 105 | #### Setup 106 | - clone github repo 107 | - `npm install` in the project folder 108 | - create `.homebridge` folder in project root 109 | - add `config.json` with appropriate content to `.homebridge` folder 110 | - run `npm run test` to start the homebridge instance for testing 111 | 112 | ## Notes 113 | ### Misc 114 | Thanks go out to "lombi" for his sony bravia homebridge plugin, which this plugin is heavily based on. 115 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "BraviaPlatform", 3 | "pluginType": "platform", 4 | "headerDisplay": "Go to [http://${{HOSTNAME}}:8999](http://${{HOSTNAME}}:8999) to enter the PIN when your TV displays it", 5 | "footerDisplay": "For documentation please see https://github.com/normen/homebridge-bravia", 6 | "schema": { 7 | "tvs": { 8 | "title": "Registered TVs", 9 | "required": false, 10 | "type": "array", 11 | "items": { 12 | "name": "TV Config", 13 | "type": "object", 14 | "properties": { 15 | "name": { 16 | "title": "Name", 17 | "description": "The name of your TV in HomeKit", 18 | "type": "string", 19 | "required": true 20 | }, 21 | "ip": { 22 | "title": "IP / Hostname", 23 | "description": "The IP address or host name of your TV, find or set in your router", 24 | "type": "string", 25 | "required": true 26 | }, 27 | "port": { 28 | "title": "Port", 29 | "description": "HTTP port of the TV", 30 | "placeholder": "80", 31 | "type": "number", 32 | "required": false 33 | }, 34 | "serverPort": { 35 | "title": "PIN entry server port", 36 | "placeholder": "8999", 37 | "type": "number", 38 | "required": false 39 | }, 40 | "mac": { 41 | "title": "MAC address", 42 | "description": "DO NOT set unless you need to use WOL", 43 | "type": "string", 44 | "required": false 45 | }, 46 | "externalaccessory": { 47 | "title": "External Accessory", 48 | "description": "Workaround for multiple TVs and remote app, registers this TV as an external accessory", 49 | "type": "boolean", 50 | "required": false 51 | }, 52 | "debug": { 53 | "title": "Debug Log Output", 54 | "description": "Gives additional debug output in the log", 55 | "type": "boolean", 56 | "required": false 57 | }, 58 | "soundoutput": { 59 | "title": "Sound Output", 60 | "description": "Required for volume control", 61 | "type": "string", 62 | "default": "speaker", 63 | "oneOf": [ 64 | { "title": "Speaker", "enum": ["speaker"] }, 65 | { "title": "Headphone", "enum": ["headphone"] } 66 | ], 67 | "required": false 68 | }, 69 | "tvsource": { 70 | "title": "TV Source", 71 | "description": "Select TV input to display TV channels in the input list", 72 | "type":"string", 73 | "oneOf": [ 74 | { "title": "Satellite", "enum": ["tv:dvbs"] }, 75 | { "title": "Antenna", "enum": ["tv:dvbt"] }, 76 | { "title": "Cable", "enum": ["tv:dvbc"] } 77 | ], 78 | "required": false 79 | }, 80 | "applications": { 81 | "title": "App", 82 | "description": "Leave empty to not list TV apps in the input list, add app names to show those apps. A part of the name should suffice.", 83 | "type": "array", 84 | "items":{ 85 | "type": "object", 86 | "properties": { 87 | "title": { 88 | "title": "App Name", 89 | "type": "string" 90 | } 91 | } 92 | }, 93 | "required": false 94 | }, 95 | "sources": { 96 | "title": "Source", 97 | "description":"Defaults to: [extInput:hdmi, extInput:component, extInput:scart, extInput:cec, extInput:widi]. Adding entries to the list below will only show those inputs.", 98 | "type": "array", 99 | "required": false, 100 | "items":{ 101 | "title": "Source Name", 102 | "type": "string" 103 | } 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "form": [ 110 | { 111 | "key":"tvs", 112 | "items":[ 113 | "tvs[].name", 114 | "tvs[].ip", 115 | "tvs[].tvsource", 116 | "tvs[].soundoutput", 117 | "tvs[].port", 118 | "tvs[].mac", 119 | "tvs[].serverPort", 120 | "tvs[].externalaccessory", 121 | "tvs[].debug", 122 | { 123 | "key":"tvs[].applications", 124 | "items":[ 125 | { 126 | "key":"tvs[].applications[]", 127 | "items":[ 128 | "tvs[].applications[].title" 129 | ] 130 | } 131 | ] 132 | }, 133 | { 134 | "key":"tvs[].sources", 135 | "items":["tvs[].sources[]"] 136 | } 137 | ] 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var url = require('url'); 4 | var base64 = require('base-64'); 5 | var wol = require('wake_on_lan'); 6 | var fs = require('fs'); 7 | const os = require('os'); 8 | 9 | var Service, Characteristic, Accessory, UUIDGen, STORAGE_PATH; 10 | 11 | class BraviaPlatform { 12 | constructor(log, config, api) { 13 | if (!config || !api) 14 | return; 15 | this.log = log; 16 | this.config = config; 17 | this.api = api; 18 | if (!config.tvs) { 19 | log('Warning: Bravia plugin not configured.'); 20 | return; 21 | } 22 | this.devices = []; 23 | const self = this; 24 | api.on('didFinishLaunching', function () { 25 | self.config.tvs.forEach(function (tv) { 26 | if (self.devices.find(device => device.name === tv.name) == undefined) { 27 | self.devices.push(new SonyTV(self, tv)); 28 | } 29 | }); 30 | self.devices.forEach(device => device.start()); 31 | }); 32 | } 33 | // called by homebridge when a device is restored from cache 34 | configureAccessory(accessory) { 35 | const self = this; 36 | if (!this.config || !this.config.tvs) { // happens if plugin is disabled and still active accessories 37 | return; 38 | } 39 | var existingConfig = this.config.tvs.find(tv => tv.name === accessory.context.config.name); 40 | if (existingConfig === undefined) { 41 | this.log('Removing TV ' + accessory.displayName + ' from HomeKit'); 42 | this.api.on('didFinishLaunching', function () { 43 | if (!accessory.context.isexternal) { 44 | self.api.unregisterPlatformAccessories('homebridge-bravia', 'BraviaPlatform', [accessory]); 45 | } else { 46 | // TODO: delete context file? not here, we're not called 47 | } 48 | }); 49 | } else { 50 | this.log('Restoring ' + accessory.displayName + ' from HomeKit'); 51 | // TODO: reachable 52 | accessory.reachable = true; 53 | // if its restored its registered 54 | self.devices.push(new SonyTV(this, existingConfig, accessory)); 55 | accessory.context.isRegisteredInHomeKit = true; 56 | } 57 | } 58 | } 59 | 60 | 61 | // TV accessory class 62 | class SonyTV { 63 | constructor(platform, config, accessory = null) { 64 | this.platform = platform; 65 | this.debug = config.debug; 66 | this.log = platform.log; 67 | this.config = config; 68 | this.name = config.name; 69 | this.ip = config.ip; 70 | this.mac = config.mac || null; 71 | this.woladdress = config.woladdress || '255.255.255.255'; 72 | this.port = config.port || '80'; 73 | this.tvsource = config.tvsource || null; 74 | this.soundoutput = config.soundoutput || 'speaker'; 75 | this.updaterate = config.updaterate || 5000; 76 | this.channelupdaterate = config.channelupdaterate === undefined ? 30000 : config.channelupdaterate; 77 | this.starttimeout = config.starttimeout || 5000; 78 | this.comp = config.compatibilitymode; 79 | this.serverPort = config.serverPort || 8999; 80 | this.sources = config.sources || ['extInput:hdmi', 'extInput:component', 'extInput:scart', 'extInput:cec', 'extInput:widi']; 81 | this.useApps = (isNull(config.applications)) ? false : (config.applications instanceof Array == true ? config.applications.length > 0 : config.applications); 82 | this.applications = (isNull(config.applications) || (config.applications instanceof Array != true)) ? [] : config.applications; 83 | this.cookiepath = STORAGE_PATH + '/sonycookie_' + this.name; 84 | 85 | this.cookie = null; 86 | this.pwd = config.pwd || null; 87 | this.registercheck = false; 88 | this.authok = false; 89 | this.appsLoaded = false; 90 | if (!this.useApps) 91 | this.appsLoaded = true; 92 | 93 | this.power = false; 94 | 95 | this.inputSourceList = []; 96 | this.inputSourceMap = new Map(); 97 | 98 | this.currentUri = null; 99 | this.currentMediaState = Characteristic.TargetMediaState.STOP; // TODO 100 | this.uriToInputSource = new Map(); 101 | 102 | this.loadCookie(); 103 | 104 | this.services = []; 105 | this.channelServices = []; 106 | this.scannedChannels = []; 107 | 108 | const contextPath = STORAGE_PATH + '/sonytv-context-' + this.name + '.json'; 109 | try { 110 | if (accessory != null) { 111 | // accessory was supplied - dynamic plugin with configureAccessory restore 112 | this.accessory = accessory; 113 | this.accessory.category = this.platform.api.hap.Categories.TELEVISION; // 31; 114 | this.grabServices(accessory); 115 | this.applyCallbacks(); 116 | } else if (this.config.externalaccessory && fs.existsSync(contextPath)) { 117 | // try and restore external accessory 118 | const rawdata = fs.readFileSync(contextPath); 119 | const accessoryContext = JSON.parse(rawdata); 120 | var uuid = UUIDGen.generate(this.name + '-SonyTV'); 121 | this.accessory = new Accessory(this.name, uuid, this.platform.api.hap.Categories.TELEVISION); 122 | this.accessory.context.uuid = accessoryContext.uuid; 123 | this.accessory.context.isexternal = true; 124 | // not registered - needs to be added 125 | // this.accessory.context.isRegisteredInHomeKit = accessoryContext.isRegisteredInHomeKit; 126 | this.accessory.context.config = this.config; 127 | this.log('Cached external accessory ' + this.name + ' found and restored'); 128 | this.createServices(); 129 | this.applyCallbacks(); 130 | this.loadChannelsFromFile(); 131 | } else { 132 | // new accessory 133 | var uuid = UUIDGen.generate(this.name + '-SonyTV'); 134 | this.log('Creating new accessory for ' + this.name); 135 | this.accessory = new Accessory(this.name, uuid, this.platform.api.hap.Categories.TELEVISION); 136 | this.accessory.context.config = config; 137 | this.accessory.context.uuid = uuidv4(); 138 | this.log('New TV ' + this.name + ', will be queried for channels/apps and added to HomeKit'); 139 | this.accessory.context.isexternal = this.config.externalaccessory; 140 | this.createServices(); 141 | this.applyCallbacks(); 142 | } 143 | } catch (e) { 144 | this.log(e); 145 | } 146 | } 147 | // get free channel identifier 148 | getFreeIdentifier() { 149 | var id = 1; 150 | var keys = [...this.inputSourceMap.keys()]; 151 | while (keys.includes(id)) { 152 | id++; 153 | } 154 | return id; 155 | } 156 | // start checking for registration and start polling status 157 | start() { 158 | this.checkRegistration(); 159 | this.updateStatus(); 160 | } 161 | // get the services (TV service, channels) from a restored HomeKit accessory 162 | grabServices(accessory) { 163 | const self = this; 164 | // FIXME: Hack, using subtype to store URI for channel 165 | accessory.services.forEach(service => { 166 | if ((service.subtype !== undefined) && service.testCharacteristic(Characteristic.Identifier)) { 167 | var identifier = service.getCharacteristic(Characteristic.Identifier).value; 168 | self.inputSourceMap.set(identifier, service); 169 | self.uriToInputSource.set(service.subtype, service); 170 | self.channelServices.push(service); 171 | } 172 | }); 173 | this.services = []; 174 | this.tvService = accessory.getService(Service.Television); 175 | this.services.push(this.tvService); 176 | this.speakerService = accessory.getService(Service.TelevisionSpeaker); 177 | this.services.push(this.speakerService); 178 | return this.services; 179 | } 180 | // create the television service for a new TV accessory 181 | createServices() { 182 | /// sony/system/ 183 | // ["getSystemInformation",[],["{\"product\":\"string\", \"region\":\"string\", \"language\":\"string\", \"model\":\"string\", \"serial\":\"string\", \"macAddr\":\"string\", \"name\":\"string\", \"generation\":\"string\", \"area\":\"string\", \"cid\":\"string\"}"],"1.0"] 184 | this.tvService = new Service.Television(this.name); 185 | this.services.push(this.tvService); 186 | this.speakerService = new Service.TelevisionSpeaker(); 187 | this.services.push(this.speakerService); 188 | // TODO: information services 189 | // var informationService = new Service.AccessoryInformation(); 190 | // informationService 191 | // .setCharacteristic(Characteristic.Manufacturer, "Sony") 192 | // .setCharacteristic(Characteristic.Model, "Android TV") 193 | // .setCharacteristic(Characteristic.SerialNumber, "12345"); 194 | // this.services.push(informationService); 195 | return this.services; 196 | } 197 | // sets the callbacks for the homebridge services to call the functions of this TV instance 198 | applyCallbacks() { 199 | this.tvService.setCharacteristic(Characteristic.ConfiguredName, this.name); 200 | this.tvService 201 | .setCharacteristic( 202 | Characteristic.SleepDiscoveryMode, 203 | Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE 204 | ); 205 | this.tvService 206 | .getCharacteristic(Characteristic.Active) 207 | .on('set', this.setPowerState.bind(this)) 208 | this.tvService.setCharacteristic(Characteristic.ActiveIdentifier, 0); 209 | this.tvService 210 | .getCharacteristic(Characteristic.ActiveIdentifier) 211 | .on('set', this.setActiveIdentifier.bind(this)) 212 | .on('get', this.getActiveIdentifier.bind(this)); 213 | this.tvService 214 | .getCharacteristic(Characteristic.RemoteKey) 215 | .on('set', this.setRemoteKey.bind(this)); 216 | this.speakerService 217 | .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE); 218 | this.speakerService 219 | .setCharacteristic(Characteristic.Name, this.soundoutput); 220 | this.speakerService 221 | .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); 222 | this.speakerService 223 | .getCharacteristic(Characteristic.VolumeSelector) // increase/decrease volume 224 | .on('set', this.setVolumeSelector.bind(this)); 225 | this.speakerService 226 | .getCharacteristic(Characteristic.Mute) 227 | .on('get', this.getMuted.bind(this)) 228 | .on('set', this.setMuted.bind(this)); 229 | this.speakerService.getCharacteristic(Characteristic.Volume) 230 | .on('get', this.getVolume.bind(this)) 231 | .on('set', this.setVolume.bind(this)); 232 | } 233 | // Do TV status check every 5 seconds 234 | updateStatus() { 235 | var that = this; 236 | setTimeout(function () { 237 | that.getPowerState(null); 238 | that.pollPlayContent(); 239 | that.updateStatus(); 240 | }, this.updaterate); 241 | } 242 | // Check if we already registered with the TV 243 | checkRegistration() { 244 | const self = this; 245 | this.registercheck = true; 246 | var clientId = 'HomeBridge-Bravia' + ':' + this.accessory.context.uuid; 247 | var post_data = '{"id":8,"method":"actRegister","version":"1.0","params":[{"clientid":"' + clientId + '","nickname":"homebridge"},[{"clientid":"' + clientId + '","value":"yes","nickname":"homebridge","function":"WOL"}]]}'; 248 | var onError = function (err) { 249 | self.log('Error: ', err); 250 | return false; 251 | }; 252 | var onSucces = function (chunk) { 253 | if (chunk.indexOf('"error"') >= 0) { 254 | if (self.debug) 255 | self.log('Error? ', chunk); 256 | } 257 | if (chunk.indexOf('[]') < 0) { 258 | self.log('Need to authenticate with TV!'); 259 | self.log('Please enter the PIN that appears on your TV at http://' + os.hostname() + ':' + self.serverPort); 260 | self.server = http.createServer(function (req, res) { 261 | var urlObject = url.parse(req.url, true, false); 262 | if (urlObject.query.pin) { 263 | res.writeHead(200, {'Content-Type': 'text/html'}); 264 | res.write('
PIN ' + urlObject.query.pin + ' sent'); 265 | self.pwd = urlObject.query.pin; 266 | self.server.close(); 267 | self.checkRegistration(); 268 | } else { 269 | res.writeHead(200, {'Content-Type': 'text/html'}); 270 | res.write(''); 271 | res.end(); 272 | } 273 | }); 274 | self.server.listen(self.serverPort, function () { 275 | self.log('PIN entry web server listening'); 276 | }); 277 | self.server.on('error', function (err) { 278 | self.log('PIN entry web server error:', err); 279 | }); 280 | } else { 281 | self.authok = true; 282 | self.receiveSources(true); 283 | } 284 | }; 285 | self.makeHttpRequest(onError, onSucces, '/sony/accessControl/', post_data, false); 286 | } 287 | // creates homebridge service for TV input 288 | addInputSource(name, uri, type, configuredName = null, identifier = null) { 289 | // FIXME: Using subtype to store URI, hack! 290 | if (identifier === null) 291 | identifier = this.getFreeIdentifier(); 292 | if (configuredName === null) 293 | configuredName = name; 294 | var inputSource = new Service.InputSource(name, uri); // displayname, subtype? 295 | inputSource.setCharacteristic(Characteristic.Identifier, identifier) 296 | .setCharacteristic(Characteristic.ConfiguredName, configuredName) 297 | .setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN) 298 | .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) 299 | .setCharacteristic(Characteristic.InputSourceType, type); 300 | this.channelServices.push(inputSource); 301 | this.tvService.addLinkedService(inputSource); 302 | this.uriToInputSource.set(uri, inputSource); 303 | this.inputSourceMap.set(identifier, inputSource); 304 | this.accessory.addService(inputSource); 305 | this.log('Added input ' + name); // +" with URI "+uri); 306 | } 307 | haveChannel(source) { 308 | return this.scannedChannels.find(channel => ( 309 | (source.subtype == channel[1]) && 310 | (source.getCharacteristic(Characteristic.InputSourceType).value == channel[2]) 311 | )) !== undefined; 312 | } 313 | haveInputSource(name, uri, type) { 314 | return this.channelServices.find(source => ( 315 | (source.subtype == uri) && 316 | (source.getCharacteristic(Characteristic.InputSourceType).value == type) 317 | )) !== undefined; 318 | } 319 | // save channels to file for external accessories 320 | saveChannelsToFile() { 321 | const storeObject = []; 322 | this.channelServices.forEach(service => { 323 | storeObject.push({ 324 | identifier: service.getCharacteristic(Characteristic.Identifier).value, 325 | name: service.getCharacteristic(Characteristic.Name).value, 326 | configuredName: service.getCharacteristic(Characteristic.ConfiguredName).value, 327 | uri: service.subtype, 328 | type: service.getCharacteristic(Characteristic.InputSourceType).value 329 | }); 330 | }); 331 | try { 332 | const data = JSON.stringify(storeObject); 333 | fs.writeFileSync(STORAGE_PATH + '/sonytv-channels-' + this.name + '.json', data); 334 | if (this.debug) 335 | this.log('Stored channels in external storage'); 336 | } catch (e) { 337 | this.log(e); 338 | } 339 | } 340 | // load channels from file for external accessories 341 | loadChannelsFromFile() { 342 | const self = this; 343 | const channelsPath = STORAGE_PATH + '/sonytv-channels-' + this.name + '.json'; 344 | try { 345 | if (fs.existsSync(channelsPath)) { 346 | const rawdata = fs.readFileSync(channelsPath); 347 | const storeObject = JSON.parse(rawdata); 348 | storeObject.forEach(source => { 349 | self.scannedChannels.push([source.name, source.uri, source.type]); 350 | self.addInputSource(source.name, source.uri, source.type, source.configuredName, source.identifier); 351 | }); 352 | if (this.debug) 353 | this.log('Loaded channels from external storage'); 354 | } 355 | } catch (e) { 356 | this.log(e); 357 | } 358 | } 359 | // syncs the channels and publishes/updates the TV accessory for HomeKit 360 | syncAccessory() { 361 | const self = this; 362 | var changeDone = false; 363 | // add new channels 364 | this.scannedChannels.forEach(channel => { 365 | if (!self.haveInputSource(channel[0], channel[1], channel[2])) { 366 | self.addInputSource(channel[0], channel[1], channel[2]); 367 | changeDone = true; 368 | } 369 | }); 370 | // remove old channels 371 | this.channelServices.forEach((service, idx, obj) => { 372 | if (!self.haveChannel(service)) { 373 | // TODO: make this function? 374 | self.tvService.removeLinkedService(service); 375 | self.accessory.removeService(service); 376 | self.inputSourceMap.delete(service.getCharacteristic(Characteristic.Identifier).value); 377 | self.uriToInputSource.delete(service.subtype); 378 | self.log('Removing nonexisting channel ' + service.getCharacteristic(Characteristic.ConfiguredName).value); 379 | obj.splice(idx, 1); 380 | changeDone = true; 381 | } 382 | }); 383 | if (!this.accessory.context.isRegisteredInHomeKit) { 384 | // add base services that haven't been added yet 385 | this.services.forEach(service => { 386 | try { 387 | if (!self.accessory.services.includes(service)) { 388 | self.log('Adding base service to accessory'); 389 | self.accessory.addService(service); 390 | changeDone = true; 391 | } 392 | } catch (e) { 393 | self.log('Can\'t add service!'); 394 | self.log(e); 395 | } 396 | }); 397 | this.log('Registering HomeBridge Accessory for ' + this.name); 398 | this.accessory.context.isRegisteredInHomeKit = true; 399 | if (!this.accessory.context.isexternal) { 400 | this.platform.api.registerPlatformAccessories('homebridge-bravia', 'BraviaPlatform', [this.accessory]); 401 | } else { 402 | try { 403 | const data = JSON.stringify(this.accessory.context); 404 | fs.writeFileSync(STORAGE_PATH + '/sonytv-context-' + this.accessory.context.config.name + '.json', data); 405 | } catch (e) { 406 | this.log(e); 407 | } 408 | this.platform.api.publishExternalAccessories('homebridge-bravia', [this.accessory]); 409 | } 410 | } else if (changeDone) { 411 | this.log('Updating HomeBridge Accessory for ' + this.name); 412 | this.platform.api.updatePlatformAccessories([this.accessory]); 413 | } 414 | if (this.accessory.context.isexternal) { 415 | this.saveChannelsToFile(); 416 | } 417 | this.receivingSources = false; 418 | } 419 | // initialize a scan for new sources 420 | receiveSources(checkPower = null) { 421 | if (checkPower === null) 422 | checkPower = this.power; 423 | if (!this.receivingSources && checkPower) { 424 | const that = this; 425 | this.inputSourceList = []; 426 | this.sources.forEach(function (sourceName) { 427 | that.inputSourceList.push(new InputSource(sourceName, getSourceType(sourceName))); 428 | }); 429 | if (!isNull(this.tvsource)) { 430 | this.inputSourceList.push(new InputSource(this.tvsource, getSourceType(this.tvsource))); 431 | } 432 | 433 | this.receivingSources = true; 434 | this.scannedChannels = []; 435 | this.receiveNextSources(); 436 | } 437 | if (this.channelupdaterate) 438 | setTimeout(this.receiveSources.bind(this), this.channelupdaterate); 439 | } 440 | // receive the next sources in the inputSourceList, register accessory if all have been received 441 | receiveNextSources() { 442 | if (this.inputSourceList.length == 0) { 443 | if (this.useApps && !this.appsLoaded) { 444 | this.receiveApplications(); 445 | } else { 446 | this.syncAccessory(); 447 | } 448 | return; 449 | } 450 | var source = this.inputSourceList.shift(); 451 | if (!isNull(source)) { 452 | this.receiveSource(source.name, source.type); 453 | } 454 | } 455 | // TV http call to receive input list for source 456 | receiveSource(sourceName, sourceType) { 457 | const that = this; 458 | var onError = function (err) { 459 | if (that.debug) 460 | that.log('Error loading sources for ' + sourceName); 461 | if (that.debug) 462 | that.log(err); 463 | that.receiveNextSources(); 464 | }; 465 | var onSucces = function (data) { 466 | try { 467 | if (data.indexOf('"error"') < 0) { 468 | var jayons = JSON.parse(data); 469 | var reslt = jayons.result[0]; 470 | reslt.forEach(function (source) { 471 | that.scannedChannels.push([source.title, source.uri, sourceType]); 472 | }); 473 | } else if (that.debug) { 474 | that.log('Can\'t load sources for ' + sourceName); 475 | that.log('TV response:'); 476 | that.log(data); 477 | } 478 | } catch (e) { 479 | if (that.debug) 480 | that.log(e); 481 | } 482 | that.receiveNextSources(); 483 | }; 484 | var post_data = '{"id":13,"method":"getContentList","version":"1.0","params":[{ "source":"' + sourceName + '","stIdx": 0}]}'; 485 | that.makeHttpRequest(onError, onSucces, '/sony/avContent', post_data, false); 486 | } 487 | // TV https call to receive application list 488 | receiveApplications() { 489 | const that = this; 490 | var onError = function (err) { 491 | if (that.debug) 492 | that.log('Error loading applications:'); 493 | if (that.debug) 494 | that.log(err); 495 | that.syncAccessory(); 496 | }; 497 | var onSucces = function (data) { 498 | try { 499 | if (data.indexOf('"error"') < 0) { 500 | var jayons = JSON.parse(data); 501 | var reslt = jayons.result[0]; 502 | reslt.sort(source => source.title).forEach(function (source) { 503 | if (that.applications.length == 0 || that.applications.map(app => app.title).filter(title => source.title.includes(title)).length > 0) { 504 | that.scannedChannels.push([source.title, source.uri, Characteristic.InputSourceType.APPLICATION]); 505 | } else { 506 | // that.log('Ignoring application: ' + source.title); 507 | } 508 | }); 509 | } else if (that.debug) { 510 | that.log('Can\'t load applications.'); 511 | that.log('TV response:'); 512 | that.log(data); 513 | } 514 | } catch (e) { 515 | if (that.debug) 516 | that.log(e); 517 | } 518 | that.syncAccessory(); 519 | }; 520 | var post_data = '{"id":13,"method":"getApplicationList","version":"1.0","params":[]}'; 521 | that.makeHttpRequest(onError, onSucces, '/sony/appControl', post_data, false); 522 | } 523 | // TV http call to poll play content 524 | pollPlayContent() { 525 | // TODO: check app list if no play content for currentUri 526 | const that = this; 527 | var post_data = '{"id":13,"method":"getPlayingContentInfo","version":"1.0","params":[]}'; 528 | var onError = function (err) { 529 | if (that.debug) 530 | that.log('Error: ', err); 531 | if (!isNull(that.currentUri)) { 532 | that.currentUri = null; 533 | that.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(0); 534 | } 535 | }; 536 | var onSucces = function (chunk) { 537 | if (chunk.indexOf('"error"') >= 0) { 538 | // happens when TV display is off 539 | if (!isNull(that.currentUri)) { 540 | that.currentUri = null; 541 | that.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(0); 542 | } 543 | } else { 544 | try { 545 | var jason = JSON.parse(chunk); 546 | if (!isNull(jason) && jason.result) { 547 | var result = jason.result[0]; 548 | var uri = result.uri; 549 | if (that.currentUri != uri) { 550 | that.currentUri = uri; 551 | var inputSource = that.uriToInputSource.get(uri); 552 | if (inputSource) { 553 | var id = inputSource.getCharacteristic(Characteristic.Identifier).value; 554 | if (!isNull(inputSource)) { 555 | that.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(id); 556 | } 557 | } 558 | } 559 | } 560 | } catch (e) { 561 | if (!isNull(that.currentUri)) { 562 | that.currentUri = null; 563 | that.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(0); 564 | } 565 | if (that.debug) 566 | that.log('Can\'t poll play content', e); 567 | } 568 | } 569 | }; 570 | that.makeHttpRequest(onError, onSucces, '/sony/avContent/', post_data, false); 571 | } 572 | // TV http call to set play content 573 | setPlayContent(uri) { 574 | const that = this; 575 | var post_data = '{"id":13,"method":"setPlayContent","version":"1.0","params":[{ "uri": "' + uri + '" }]}'; 576 | var onError = function (err) { 577 | that.log('Error setting play content: ', err); 578 | }; 579 | var onSucces = function (chunk) { 580 | }; 581 | that.makeHttpRequest(onError, onSucces, '/sony/avContent/', post_data, true); 582 | } 583 | // TV http call to set the active app 584 | setActiveApp(uri) { 585 | const that = this; 586 | var post_data = '{"id":13,"method":"setActiveApp","version":"1.0","params":[{"uri":"' + uri + '"}]}'; 587 | var onError = function (err) { 588 | that.log('Error setting active app: ', err); 589 | }; 590 | var onSucces = function (data) { 591 | }; 592 | that.makeHttpRequest(onError, onSucces, '/sony/appControl', post_data, true); 593 | } 594 | // homebridge callback to get current channel identifier 595 | getActiveIdentifier(callback) { 596 | var uri = this.currentUri; 597 | if (!isNull(uri)) { 598 | var inputSource = this.uriToInputSource.get(uri); 599 | if (inputSource) { 600 | var id = inputSource.getCharacteristic(Characteristic.Identifier).value; 601 | if (!isNull(inputSource)) { 602 | if (!isNull(callback)) 603 | callback(null, id); 604 | return; 605 | } 606 | } 607 | } 608 | if (!isNull(callback)) 609 | callback(null, 0); 610 | } 611 | // homebridge callback to set current channel 612 | setActiveIdentifier(identifier, callback) { 613 | var inputSource = this.inputSourceMap.get(identifier); 614 | if (inputSource && inputSource.testCharacteristic(Characteristic.InputSourceType)) { 615 | if (inputSource.getCharacteristic(Characteristic.InputSourceType).value == Characteristic.InputSourceType.APPLICATION) { 616 | this.setActiveApp(inputSource.subtype); 617 | } else { 618 | this.setPlayContent(inputSource.subtype); 619 | } 620 | } 621 | if (!isNull(callback)) 622 | callback(null); 623 | } 624 | // homebridge callback to set volume via selector (up/down) 625 | setVolumeSelector(key, callback) { 626 | const that = this; 627 | var value = ''; 628 | var onError = function (err) { 629 | that.log(err); 630 | if (!isNull(callback)) 631 | callback(null); 632 | }; 633 | var onSucces = function (data) { 634 | if (!isNull(callback)) 635 | callback(null); 636 | }; 637 | switch (key) { 638 | case Characteristic.VolumeSelector.INCREMENT: // Volume up 639 | value = 'AAAAAQAAAAEAAAASAw=='; 640 | break; 641 | case Characteristic.VolumeSelector.DECREMENT: // Volume down 642 | value = 'AAAAAQAAAAEAAAATAw=='; 643 | break; 644 | } 645 | var post_data = that.createIRCC(value); 646 | that.makeHttpRequest(onError, onSucces, '', post_data, false); 647 | } 648 | // homebridge callback to set pressed key 649 | setRemoteKey(key, callback) { 650 | var value = ''; 651 | var that = this; 652 | var onError = function (err) { 653 | that.log(err); 654 | if (!isNull(callback)) 655 | callback(null); 656 | }; 657 | var onSucces = function (data) { 658 | if (!isNull(callback)) 659 | callback(null); 660 | }; 661 | // https://gist.github.com/joshluongo/51dcfbe5a44ee723dd32 662 | switch (key) { 663 | case Characteristic.RemoteKey.REWIND: 664 | value = 'AAAAAgAAAJcAAAAbAw=='; 665 | break; 666 | case Characteristic.RemoteKey.FAST_FORWARD: 667 | value = 'AAAAAgAAAJcAAAAcAw=='; 668 | break; 669 | case Characteristic.RemoteKey.NEXT_TRACK: 670 | value = 'AAAAAgAAAJcAAAA9Aw=='; 671 | break; 672 | case Characteristic.RemoteKey.PREVIOUS_TRACK: 673 | value = 'AAAAAgAAAJcAAAB5Aw=='; 674 | break; 675 | case Characteristic.RemoteKey.ARROW_UP: 676 | value = 'AAAAAQAAAAEAAAB0Aw=='; 677 | break; 678 | case Characteristic.RemoteKey.ARROW_DOWN: 679 | value = 'AAAAAQAAAAEAAAB1Aw=='; 680 | break; 681 | case Characteristic.RemoteKey.ARROW_LEFT: 682 | value = 'AAAAAQAAAAEAAAA0Aw=='; 683 | break; 684 | case Characteristic.RemoteKey.ARROW_RIGHT: 685 | value = 'AAAAAQAAAAEAAAAzAw=='; 686 | break; 687 | case Characteristic.RemoteKey.SELECT: 688 | value = 'AAAAAQAAAAEAAABlAw=='; 689 | break; 690 | case Characteristic.RemoteKey.BACK: 691 | value = 'AAAAAgAAAJcAAAAjAw=='; 692 | break; 693 | case Characteristic.RemoteKey.EXIT: 694 | value = 'AAAAAQAAAAEAAABjAw=='; 695 | break; 696 | case Characteristic.RemoteKey.PLAY_PAUSE: 697 | value = 'AAAAAgAAAJcAAAAaAw=='; 698 | break; 699 | case Characteristic.RemoteKey.INFORMATION: 700 | value = 'AAAAAQAAAAEAAAA6Aw=='; 701 | break; 702 | } 703 | var post_data = that.createIRCC(value); 704 | that.makeHttpRequest(onError, onSucces, '', post_data, false); 705 | } 706 | // homebridge callback to get muted state 707 | getMuted(callback) { 708 | var that = this; 709 | if (!that.power) { 710 | if (!isNull(callback)) 711 | callback(null, 0); 712 | return; 713 | } 714 | var post_data = '{"id":4,"method":"getVolumeInformation","version":"1.0","params":[]}'; 715 | var onError = function (err) { 716 | if (that.debug) 717 | that.log('Error: ', err); 718 | if (!isNull(callback)) 719 | callback(null, false); 720 | }; 721 | var onSucces = function (chunk) { 722 | if (chunk.indexOf('"error"') >= 0) { 723 | if (that.debug) 724 | that.log('Error? ', chunk); 725 | if (!isNull(callback)) 726 | callback(null, false); 727 | return; 728 | } 729 | var _json = null; 730 | try { 731 | _json = JSON.parse(chunk); 732 | } catch (e) { 733 | if (!isNull(callback)) 734 | callback(null, false); 735 | return; 736 | } 737 | if (isNull(_json.result)) { 738 | if (!isNull(callback)) 739 | callback(null, false); 740 | return; 741 | } 742 | for (var i = 0; i < _json.result[0].length; i++) { 743 | var volume = _json.result[0][i].volume; 744 | var typ = _json.result[0][i].target; 745 | if (typ === that.soundoutput) { 746 | if (!isNull(callback)) 747 | callback(null, _json.result[0][i].mute); 748 | return; 749 | } 750 | } 751 | if (!isNull(callback)) 752 | callback(null, false); 753 | }; 754 | that.makeHttpRequest(onError, onSucces, '/sony/audio/', post_data, false); 755 | } 756 | // homebridge callback to set muted state 757 | setMuted(muted, callback) { 758 | var that = this; 759 | if (!that.power) { 760 | if (!isNull(callback)) 761 | callback(null); 762 | return; 763 | } 764 | var merterd = muted ? 'true' : 'false'; 765 | var post_data = '{"id":13,"method":"setAudioMute","version":"1.0","params":[{"status":' + merterd + '}]}'; 766 | var onError = function (err) { 767 | if (that.debug) 768 | that.log('Error: ', err); 769 | if (!isNull(callback)) 770 | callback(null); 771 | }; 772 | var onSucces = function (chunk) { 773 | if (chunk.indexOf('"error"') >= 0) { 774 | if (that.debug) 775 | that.log('Error? ', chunk); 776 | } 777 | if (!isNull(callback)) 778 | callback(null); 779 | }; 780 | that.makeHttpRequest(onError, onSucces, '/sony/audio/', post_data, false); 781 | } 782 | // homebridge callback to get absoluet volume 783 | getVolume(callback) { 784 | var that = this; 785 | if (!that.power) { 786 | if (!isNull(callback)) 787 | callback(null, 0); 788 | return; 789 | } 790 | var post_data = '{"id":4,"method":"getVolumeInformation","version":"1.0","params":[]}'; 791 | var onError = function (err) { 792 | if (that.debug) 793 | that.log('Error: ', err); 794 | if (!isNull(callback)) 795 | callback(null, 0); 796 | }; 797 | var onSucces = function (chunk) { 798 | if (chunk.indexOf('"error"') >= 0) { 799 | if (that.debug) 800 | that.log('Error? ', chunk); 801 | if (!isNull(callback)) 802 | callback(null, 0); 803 | return; 804 | } 805 | var _json = null; 806 | try { 807 | _json = JSON.parse(chunk); 808 | } catch (e) { 809 | if (!isNull(callback)) 810 | callback(null, 0); 811 | return; 812 | } 813 | if (isNull(_json.result)) { 814 | if (!isNull(callback)) 815 | callback(null, 0); 816 | return; 817 | } 818 | for (var i = 0; i < _json.result[0].length; i++) { 819 | var volume = _json.result[0][i].volume; 820 | var typ = _json.result[0][i].target; 821 | if (typ === that.soundoutput) { 822 | if (!isNull(callback)) 823 | callback(null, volume); 824 | return; 825 | } 826 | } 827 | if (!isNull(callback)) 828 | callback(null, 0); 829 | }; 830 | that.makeHttpRequest(onError, onSucces, '/sony/audio/', post_data, false); 831 | } 832 | // homebridge callback to set absolute volume 833 | setVolume(volume, callback) { 834 | var that = this; 835 | if (!that.power) { 836 | if (!isNull(callback)) 837 | callback(null); 838 | return; 839 | } 840 | var post_data = '{"id":13,"method":"setAudioVolume","version":"1.0","params":[{"target":"' + that.soundoutput + '","volume":"' + volume + '"}]}'; 841 | var onError = function (err) { 842 | if (that.debug) 843 | that.log('Error: ', err); 844 | if (!isNull(callback)) 845 | callback(null); 846 | }; 847 | var onSucces = function (chunk) { 848 | if (!isNull(callback)) 849 | callback(null); 850 | }; 851 | that.makeHttpRequest(onError, onSucces, '/sony/audio/', post_data, false); 852 | } 853 | // homebridge callback to get power state 854 | getPowerState(callback) { 855 | var that = this; 856 | var onError = function (err) { 857 | if (that.debug) 858 | that.log('Error: ', err); 859 | if (!isNull(callback)) 860 | callback(null, false); 861 | that.updatePowerState(false); 862 | }; 863 | var onSucces = function (chunk) { 864 | var _json = null; 865 | try { 866 | _json = JSON.parse(chunk); 867 | if (!isNull(_json) && !isNull(_json.result[0]) && _json.result[0].status === 'active') { 868 | that.updatePowerState(true); 869 | if (!isNull(callback)) 870 | callback(null, true); 871 | } else { 872 | that.updatePowerState(false); 873 | if (!isNull(callback)) 874 | callback(null, false); 875 | } 876 | } catch (e) { 877 | if (that.debug) 878 | console.log(e); 879 | that.updatePowerState(false); 880 | if (!isNull(callback)) 881 | callback(null, false); 882 | } 883 | }; 884 | try { 885 | var post_data = '{"id":2,"method":"getPowerStatus","version":"1.0","params":[]}'; 886 | that.makeHttpRequest(onError, onSucces, '/sony/system/', post_data, false); 887 | } catch (globalExcp) { 888 | if (that.debug) 889 | console.log(globalExcp); 890 | that.updatePowerState(false); 891 | if (!isNull(callback)) 892 | callback(null, false); 893 | } 894 | } 895 | // homebridge callback to set power state 896 | setPowerState(state, callback) { 897 | var that = this; 898 | var onError = function (err) { 899 | if (that.debug) 900 | that.log('Error: ', err); 901 | if (!isNull(callback)) 902 | callback(null); 903 | }; 904 | var onSucces = function (chunk) { 905 | if (!isNull(callback)) 906 | callback(null); 907 | }; 908 | var onWol = function (error) { 909 | if (error) 910 | that.log('Error when sending WOL packets', error); 911 | if (!isNull(callback)) 912 | callback(null); 913 | }; 914 | if (state) { 915 | if (!isNull(this.mac)) { 916 | wol.wake(this.mac, {address: this.woladdress}, onWol); 917 | } else { 918 | var post_data = '{"id":2,"method":"setPowerStatus","version":"1.0","params":[{"status":true}]}'; 919 | that.makeHttpRequest(onError, onSucces, '/sony/system/', post_data, false); 920 | } 921 | } else { 922 | if (!isNull(this.mac)) { 923 | var post_data = this.createIRCC('AAAAAQAAAAEAAAAvAw=='); 924 | this.makeHttpRequest(onError, onSucces, '', post_data, false); 925 | } else { 926 | var post_data = '{"id":2,"method":"setPowerStatus","version":"1.0","params":[{"status":false}]}'; 927 | that.makeHttpRequest(onError, onSucces, '/sony/system/', post_data, false); 928 | } 929 | } 930 | } 931 | // sends the current power state to homebridge 932 | updatePowerState(state) { 933 | if (this.power != state) { 934 | this.power = state; 935 | this.tvService.getCharacteristic(Characteristic.Active).updateValue(this.power); 936 | } 937 | } 938 | // make http request to TV 939 | makeHttpRequest(errcallback, resultcallback, url, post_data, canTurnTvOn) { 940 | var that = this; 941 | var data = ''; 942 | if (isNull(canTurnTvOn)) {canTurnTvOn = false;} 943 | if (!that.power && canTurnTvOn) { 944 | that.setPowerState(true, null); 945 | var timeout = that.starttimeout; 946 | setTimeout(function () { 947 | that.makeHttpRequest(errcallback, resultcallback, url, post_data, false); 948 | }, timeout); 949 | return; 950 | } 951 | var post_options = that.getPostOptions(url); 952 | var post_req = http.request(post_options, function (res) { 953 | that.setCookie(res.headers); 954 | res.setEncoding('utf8'); 955 | res.on('data', function (chunk) { 956 | data += chunk; 957 | }); 958 | res.on('end', function () { 959 | if (!isNull(resultcallback)) { 960 | resultcallback(data); 961 | } 962 | }); 963 | }); 964 | try { 965 | post_req.on('error', function (err) { 966 | if (!isNull(errcallback)) { 967 | errcallback(err); 968 | } 969 | }); 970 | post_req.write(post_data); 971 | post_req.end(); 972 | } catch (e) { 973 | if (!isNull(errcallback)) { 974 | errcallback(e); 975 | } 976 | } 977 | } 978 | // helper to create IRCC command string 979 | createIRCC(command) { 980 | return '