homebridge-ziggo-next
Version:
homebridge-plugin - Add Ziggo Mediabox Next to Homekit
536 lines (433 loc) • 15 kB
JavaScript
const mqtt = require('mqtt');
const request = require('request-promise');
const _ = require('underscore');
const express = require('express');
const bodyParser = require('body-parser');
const varClientId = makeId(30);
const sessionUrl = 'https://web-api-prod-obo.horizon.tv/oesp/v3/NL/nld/web/session';
const jwtUrl = 'https://web-api-prod-obo.horizon.tv/oesp/v3/NL/nld/web/tokens/jwt';
const channelsUrl = 'https://web-api-prod-obo.horizon.tv/oesp/v3/NL/nld/web/channels';
const mqttUrl = 'wss://obomsg.prod.nl.horizon.tv:443/mqtt';
let mqttClient = {};
let ziggoUsername;
let ziggoPassword;
let console;
let Service;
let Characteristic;
let mqttUsername;
let mqttPassword;
let setopboxId;
let setopboxState;
let stations = [];
let uiStatus;
let currentChannel;
let currentChannelId;
let currentState;
const sessionRequestOptions = {
method: 'POST',
uri: sessionUrl,
body: {
username: ziggoUsername,
password: ziggoPassword
},
json: true
};
function makeId(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
// --== MAIN SETUP ==--
function ZiggoPlatform(log, config) {
console = log;
this.log = log;
this.config = config;
}
/* Initialise Accessory */
function ZiggoAccessory(log, config) {
this.log = log;
this.config = config;
this.sysConfig = null;
this.name = config.name || 'Ziggo Next';
this.inputs = [];
this.enabledServices = [];
this.inputServices = [];
this.playing = true;
// Configuration
ziggoUsername = this.config.username || '';
ziggoPassword = this.config.username || '';
// this.getChannels();
this.getSession();
this.setInputs();
// Check & Update Accessory Status every 5 seconds
this.checkStateInterval = setInterval(
this.updateZiggoState.bind(this),
5000,
);
}
module.exports = (homebridge) => {
({ Service, Characteristic } = homebridge.hap);
homebridge.registerPlatform('homebridge-ziggo-next', 'ziggo-next', ZiggoPlatform);
};
ZiggoPlatform.prototype = {
accessories(callback) {
callback([
new ZiggoAccessory(
this.log,
this.config
),
]);
},
};
ZiggoAccessory.prototype = {
/* Services */
getServices() {
this.informationService();
this.televisionService();
this.televisionSpeakerService();
this.inputSourceServices();
// this.volumeService();
return this.enabledServices;
},
informationService() {
// Create Information Service
this.informationService = new Service.AccessoryInformation();
this.informationService
.setCharacteristic(Characteristic.Name, this.name)
.setCharacteristic(Characteristic.Manufacturer, 'Ziggo')
// .setCharacteristic(Characteristic.FirmwareRevision, require('./package.json').version)
.setCharacteristic(Characteristic.Model, 'Mediabox Next')
.setCharacteristic(Characteristic.SerialNumber, 'Unknown');
this.enabledServices.push(this.informationService);
},
televisionService() {
// Create Television Service (AVR)
this.tvService = new Service.Television(this.name, 'ziggoService');
this.tvService
.setCharacteristic(Characteristic.ConfiguredName, this.name)
.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE);
this.tvService
.getCharacteristic(Characteristic.Active)
.on('get', this.getPowerState.bind(this))
.on('set', this.setPowerState.bind(this));
this.tvService
.getCharacteristic(Characteristic.ActiveIdentifier)
.on('get', this.getInputState.bind(this))
.on('set', (inputIdentifier, callback) => {
this.setInputState(this.inputs[inputIdentifier], callback);
});
this.tvService
.getCharacteristic(Characteristic.RemoteKey)
.on('set', this.remoteKeyPress.bind(this));
this.enabledServices.push(this.tvService);
},
televisionSpeakerService() {
this.tvSpeakerService = new Service.TelevisionSpeaker(`${this.name} AVR`, 'ziggoSpeakerService');
this.tvSpeakerService
.setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE)
.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE);
this.tvSpeakerService
.getCharacteristic(Characteristic.VolumeSelector)
.on('set', (direction, callback) => {
callback();
});
this.tvService.addLinkedService(this.tvSpeakerService);
this.enabledServices.push(this.tvSpeakerService);
},
inputSourceServices() {
for (let i = 0; i < 50; i++) {
const inputService = new Service.InputSource(i, `inputSource_${i}`);
inputService
.setCharacteristic(Characteristic.Identifier, i)
.setCharacteristic(Characteristic.ConfiguredName, `Input ${i < 9 ? `0${i + 1}` : i + 1}`)
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.NOT_CONFIGURED)
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION)
.setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.HIDDEN);
inputService
.getCharacteristic(Characteristic.ConfiguredName)
.on('set', (value, callback) => {
callback(null, value);
});
this.tvService.addLinkedService(inputService);
this.inputServices.push(inputService);
this.enabledServices.push(inputService);
}
},
getSession() {
sessionRequestOptions.body.username = this.config.username;
sessionRequestOptions.body.password = this.config.password;
request(sessionRequestOptions)
.then((json) => {
//this.log(json);
sessionJson = json;
this.getJwtToken(sessionJson.oespToken, sessionJson.customer.householdId);
})
.catch((err) => {
this.log('getSession: ', err.message);
});
//return sessionJson || false;
},
getJwtToken(oespToken, householdId) {
const jwtRequestOptions = {
method: 'GET',
uri: jwtUrl,
headers: {
'X-OESP-Token': oespToken,
'X-OESP-Username': ziggoUsername
},
json: true
};
request(jwtRequestOptions)
.then(json => {
jwtJson = json;
mqttUsername = householdId;
mqttPassword = jwtJson.token;
this.startMqttClient(this);
})
.catch(function (err) {
// this.log('getJwtToken: ', err.message);
return false;
});
},
startMqttClient(parent) {
mqttClient = mqtt.connect(mqttUrl, {
connectTimeout: 10*1000, //10 seconds
clientId: varClientId,
username: mqttUsername,
password: mqttPassword
});
mqttClient.on('connect', function () {
mqttClient.subscribe(mqttUsername, function (err) {
if(err){
parent.log(err);
return false;
}
});
mqttClient.subscribe(mqttUsername +'/+/status', function (err) {
if(err){
parent.log(err);
return false;
}
});
mqttClient.on('message', function (topic, payload) {
let payloadValue = JSON.parse(payload);
if(payloadValue.deviceType){
if(payloadValue.deviceType == 'STB'){
setopboxId = payloadValue.source;
setopboxState = payloadValue.state;
if (setopboxState == 'ONLINE_RUNNING')
currentState = true;
else if (setopboxState == 'OFFLINE')
currentState = false;
mqttClient.subscribe(mqttUsername + '/' + varClientId, function (err) {
if(err){
parent.log(err);
return false;
}
});
mqttClient.subscribe(mqttUsername + '/' + setopboxId, function (err) {
if(err){
parent.log(err);
return false;
}
});
mqttClient.subscribe(mqttUsername + '/'+ setopboxId +'/status', function (err) {
if(err){
parent.log(err);
return false;
}
});
parent.getUiStatus();
}
}
if(payloadValue && payloadValue.status){
parent.log(payloadValue.status);
if(payloadValue.status.playerState) {
currentChannelId = payloadValue.status.playerState.source.channelId;
parent.log('Current channel:', currentChannelId);
}
}
});
mqttClient.on('error', function(err) {
parent.log(err);
mqttClient.end();
return false;
});
mqttClient.on('close', function () {
parent.log('Connection closed');
mqttClient.end();
return false;
});
});
},
switchChannel(channel) {
// this.log('Switch to', channel);
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.pushToTV","source":{"clientId":"' + varClientId + '","friendlyDeviceName":"HomeKit"},"status":{"sourceType":"linear","source":{"channelId":"' + channel + '"},"relativePosition":0,"speed":1}}')
},
powerKey() {
// this.log('Power on/off');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.KeyEvent","source":"' + varClientId + '","status":{"w3cKey":"Power","eventType":"keyDownUp"}}');
currentState = (currentState ? false : true);
this.tvService.getCharacteristic(Characteristic.Active).updateValue(currentState);
},
escapeKey() {
// this.log('Send escape-key');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.KeyEvent","source":"' + varClientId + '","status":{"w3cKey":"Escape","eventType":"keyDownUp"}}')
},
pauseKey() {
// this.log('Send pause-key');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.KeyEvent","source":"' + varClientId + '","status":{"w3cKey":"MediaPause","eventType":"keyDownUp"}}')
},
sendKey(key) {
// this.log('Send key');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.KeyEvent","source":"' + varClientId + '","status":{"w3cKey":"'+key+'","eventType":"keyDownUp"}}')
},
getUiStatus() {
// this.log('Get UI status');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.getUiStatus","source":"' + varClientId + '"}')
},
/* State Handlers */
updateZiggoState(error, status) {
// this.log('updateZiggoState');
this.getUiStatus();
this.setInputs();
if (this.tvService) {
this.tvService.getCharacteristic(Characteristic.Active).updateValue(currentState);
if (status && currentChannelId) {
this.inputs.filter((input, index) => {
if (input.id === currentChannelId) {
// Get and update homekit accessory with the current set input
if (this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).value !== index) {
this.log(`Updating input from ${input.name} to ${input.name}`);
return this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(index);
}
}
return null;
});
}
}
},
setInputs() {
// this.log('setInputs');
if (this.inputServices && this.inputServices.length) {
request({ url: channelsUrl, json: true}).then(availableInputs => {
const sanitizedInputs = [];
let i = 0;
availableInputs.channels.forEach(function (channel) {
if (i < 50)
{
sanitizedInputs.push({id: channel.stationSchedules[0].station.serviceId, name: channel.title, index: i});
}
i++;
});
this.inputs = sanitizedInputs;
this.inputs.forEach((input, i) => {
const inputService = this.inputServices[i];
inputService.getCharacteristic(Characteristic.ConfiguredName).updateValue( `${i < 9 ? `0${i + 1}` : i + 1}` + ". " + input.name);
inputService.getCharacteristic(Characteristic.IsConfigured).updateValue(Characteristic.IsConfigured.CONFIGURED);
inputService.getCharacteristic(Characteristic.CurrentVisibilityState).updateValue(Characteristic.CurrentVisibilityState.SHOWN);
});
},
error => {
this.log(`Failed to get available inputs from ${this.config.name}. Please verify the AVR is connected and accessible at ${this.config.ip}`);
}
);
}
},
getPowerState(callback) {
this.log('getPowerState');
callback(null, (currentState || false));
},
setPowerState(state, callback) {
this.log('Power on/off');
mqttClient.publish(mqttUsername + '/' + setopboxId, '{"id":"' + makeId(8) + '","type":"CPE.KeyEvent","source":"' + varClientId + '","status":{"w3cKey":"Power","eventType":"keyDownUp"}}');
currentState = (currentState ? false : true);
callback();
},
getInputState(callback) {
this.log('getInputState');
isDone = false;
this.inputs.filter((input, index) => {
if (input.id === currentChannelId) {
this.log(`Current Input: ${input.name}`, index);
isDone = true;
return callback(null, index);
}
});
if (!isDone)
return callback(null, null);
},
setInputState(input, callback) {
this.log('setInputState');
this.log(`Set input: ${input.name} (${input.id})`);
this.switchChannel(input.id);
callback();
},
sendRemoteCode(remoteKey, callback) {
this.log('sendRemoteCode');
callback(true);
},
remoteKeyPress(remoteKey, callback) {
switch (remoteKey) {
case Characteristic.RemoteKey.REWIND:
this.sendKey('MediaRewind');
callback();
break;
case Characteristic.RemoteKey.FAST_FORWARD:
this.sendKey('MediaFastForward');
callback();
break;
case Characteristic.RemoteKey.NEXT_TRACK:
this.sendKey('DisplaySwap');
callback();
break;
case Characteristic.RemoteKey.PREVIOUS_TRACK:
this.sendKey('DisplaySwap');
callback();
break;
case Characteristic.RemoteKey.ARROW_UP:
this.sendKey('ArrowUp');
callback();
break;
case Characteristic.RemoteKey.ARROW_DOWN:
this.sendKey('ArrowDown');
callback();
break;
case Characteristic.RemoteKey.ARROW_LEFT:
this.sendKey('ArrowLeft');
callback();
break;
case Characteristic.RemoteKey.ARROW_RIGHT:
this.sendKey('ArrowRight');
callback();
break;
case Characteristic.RemoteKey.SELECT:
this.sendKey('Enter');
callback();
break;
case Characteristic.RemoteKey.BACK:
this.sendKey('Exit');
callback();
break;
case Characteristic.RemoteKey.EXIT:
this.sendKey('Exit');
callback();
break;
case Characteristic.RemoteKey.PLAY_PAUSE:
this.sendKey('MediaPlay');
callback();
break;
case Characteristic.RemoteKey.INFORMATION:
this.sendKey('Info');
callback();
break;
default:
callback();
break;
}
},
};