UNPKG

redmatic-homekit

Version:

HAP-Nodejs based Node-RED nodes to create HomeKit Accessories

273 lines (225 loc) 11.5 kB
const path = require('path'); const net = require('net'); const hap = require('hap-nodejs'); const {uuid, Accessory, Service, Characteristic} = hap; const pkg = require('../package.json'); const accessories = {}; module.exports = function (RED) { hap.init(path.join(RED.settings.userDir, 'homekit')); RED.httpAdmin.get('/redmatic-homekit-tv', (req, res) => { if (req.query.config) { const acc = accessories[req.query.config]; if (acc) { res.status(200).send(JSON.stringify({setupURI: acc.setupURI()})); } else { res.status(500).send(JSON.stringify({})); } } else { res.status(404).send(JSON.stringify({})); } }); class RedMaticHomeKitTv { constructor(config) { RED.nodes.createNode(this, config); this.name = config.name || ('TV ' + config.id); let acc; let tvService; let speakerService; if (accessories[config.id]) { this.debug('tv already existing ' + this.name + ' ' + config.id); acc = accessories[config.id]; tvService = acc.getService('Television'); speakerService = acc.getService('Speaker'); } else { acc = new Accessory(this.name, uuid.generate(config.id)); accessories[config.id] = acc; this.debug('tv created ' + this.name + ' ' + config.id + ' ' + config.username); acc.getService(Service.AccessoryInformation) .setCharacteristic(hap.Characteristic.Manufacturer, 'RedMatic') .setCharacteristic(hap.Characteristic.Model, 'TV') .setCharacteristic(hap.Characteristic.SerialNumber, config.username) .setCharacteristic(hap.Characteristic.FirmwareRevision, pkg.version); tvService = acc.addService(Service.Television, this.name, 'Television'); tvService.setCharacteristic(Characteristic.ConfiguredName, this.name); tvService.setCharacteristic( Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE ); tvService.setCharacteristic(Characteristic.ActiveIdentifier, 1); speakerService = acc.addService(Service.TelevisionSpeaker, this.name, 'Speaker'); speakerService .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); this.debug('creating ' + config.inputsources.length + ' input sources'); config.inputsources.forEach((src, i) => { const id = i + 1; const inputService = acc.addService(Service.InputSource, src.name, src.name); inputService .setCharacteristic(Characteristic.Identifier, id) .setCharacteristic(Characteristic.ConfiguredName, src.name) .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) .setCharacteristic(Characteristic.InputSourceType, src.type) .setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN) .setCharacteristic(Characteristic.TargetVisibilityState, Characteristic.TargetVisibilityState.SHOWN); tvService.addLinkedService(inputService); }); // tvService.addLinkedService(speakerService); this.log('publishing tv ' + this.name + ' ' + config.username); const testPort = net.createServer() .once('error', err => { this.error(err); this.status({fill: 'red', shape: 'dot', text: err.message}); }) .once('listening', () => { testPort.once('close', () => { acc.publish({ username: config.username, port: config.port, pincode: config.pincode, category: Accessory.Categories.TELEVISION }); acc._server.on('listening', () => { this.log('tv ' + this.name + ' listening on port ' + config.port); this.status({shape: 'ring', fill: 'grey', text: ' '}); }); acc._server.on('pair', username => { this.log('tv ' + this.name + ' paired', username); }); acc._server.on('unpair', username => { this.log('tv ' + this.name + ' unpaired', username); }); acc._server.on('verify', () => { this.log('tv ' + this.name + ' verify'); }); }).close(); }) .listen(config.port); } const setActive = (newValue, callback) => { this.send({topic: 'Active', payload: Boolean(newValue)}); this.status({shape: newValue ? 'dot' : 'ring', fill: newValue ? 'blue' : 'grey'}); callback(null); }; const setActiveIdentifier = (newValue, callback) => { this.status({shape: 'dot', fill: 'blue', text: config.inputsources[newValue - 1].name}); this.send({topic: 'InputSource', payload: config.inputsources[newValue - 1].name, identifier: newValue}); callback(null); }; const setPowerModeSelection = (newValue, callback) => { this.send({topic: 'PowerModeSelection', payload: newValue}); callback(null); }; const setRemoteKey = (newValue, callback) => { const msg = {topic: 'RemoteKey'}; switch (newValue) { case 0: msg.payload = 'REWIND'; msg.lgtv = 'REWIND'; break; case 1: msg.payload = 'FAST_FORWARD'; msg.lgtv = 'FASTFORWARD'; break; case 2: msg.payload = 'NEXT_TRACK'; break; case 3: msg.payload = 'PREVIOUS_TRACK'; break; case 4: msg.payload = 'ARROW_UP'; msg.lgtv = 'UP'; break; case 5: msg.payload = 'ARROW_DOWN'; msg.lgtv = 'DOWN'; break; case 6: msg.payload = 'ARROW_LEFT'; msg.lgtv = 'LEFT'; break; case 7: msg.payload = 'ARROW_RIGHT'; msg.lgtv = 'RIGHT'; break; case 8: msg.payload = 'SELECT'; msg.lgtv = 'ENTER'; break; case 9: msg.payload = 'BACK'; msg.lgtv = 'BACK'; break; case 10: msg.payload = 'EXIT'; msg.lgtv = 'EXIT'; break; case 11: msg.payload = 'PLAY_PAUSE'; break; case 15: msg.payload = 'INFORMATION'; msg.lgtv = 'INFO'; break; default: } msg.characteristicValue = newValue; this.send(msg); callback(null); }; const setVolumeSelector = (newValue, callback) => { this.send({topic: 'VolumeSelector', payload: newValue ? 'VOLUMEDOWN' : 'VOLUMEUP'}); callback(null); }; this.debug('add event listeners'); tvService.getCharacteristic(Characteristic.Active) .on('set', setActive); tvService.getCharacteristic(Characteristic.ActiveIdentifier) .on('set', setActiveIdentifier); tvService.getCharacteristic(Characteristic.RemoteKey) .on('set', setRemoteKey); tvService.getCharacteristic(Characteristic.PowerModeSelection) .on('set', setPowerModeSelection); speakerService.getCharacteristic(Characteristic.VolumeSelector) .on('set', setVolumeSelector); this.on('input', msg => { switch (msg.topic) { case 'InputSource': { let identifier = msg.payload; if (typeof msg.payload === 'string') { config.inputsources.forEach((src, i) => { if (msg.payload === src.name) { identifier = i + 1; } }); } if (config.inputsources[identifier - 1]) { this.debug('set ActiveIdentifier ' + identifier + ' (payload was ' + msg.payload + ')'); this.status({shape: 'dot', fill: 'blue', text: config.inputsources[identifier - 1].name}); tvService.updateCharacteristic(Characteristic.ActiveIdentifier, identifier); } break; } default: this.debug('set Active ' + msg.payload); this.status({shape: msg.payload ? 'dot' : 'ring', fill: msg.payload ? 'blue' : 'grey'}); tvService.updateCharacteristic(Characteristic.Active, msg.payload ? 1 : 0); } }); this.on('close', () => { this.debug('remove event listeners'); tvService.getCharacteristic(Characteristic.Active) .removeListener('set', setActive); tvService.getCharacteristic(Characteristic.ActiveIdentifier) .removeListener('set', setActiveIdentifier); tvService.getCharacteristic(Characteristic.RemoteKey) .removeListener('set', setRemoteKey); tvService.getCharacteristic(Characteristic.PowerModeSelection) .removeListener('set', setPowerModeSelection); speakerService.getCharacteristic(Characteristic.VolumeSelector) .removeListener('set', setVolumeSelector); }); } } RED.nodes.registerType('redmatic-homekit-tv', RedMaticHomeKitTv); };