/****************************
* HEOS Script for ioBroker
****************************
* 23.01.2018 Uhula, MIT License, no warranty, use on your own risc
*
* Wichtig!
****************************
* (a) Im Javascript-Adapter muss in der Instanz-Konfiguration "node-ssdp" mit angegeben werden!
* (b) Wenn mit Favoriten (Presets) gearbeitet werden soll, müssen die HEOS-Konto Logindaten in
* den beiden Konstanten HEOS_USERNAME (EmailAdr) und HEOS_PASSWORD hier im Script in der
* Konfiguration angegeben werden (so, wie in der HEOS App)!
* (c) Eine Konfiguration von IP-Adressen und Player-IDs ist nicht notwendig!
* (d) U.U. kann es notwendig sein das Script nach dem 1.Start zu beenden und nach 30 Sek erneut zu starten, da
* die Neuanlage der ioBroker States etwas Zeit benötigt. Warnungen sind dabei zu ignorieren. ;-)
*
*
* Funktion
****************************
* Das Script stellt zwei JS Klassen zur Steuerung der Denon HEOS Geräte zur Verfügung. Die
* class Heos dient dabei dem Erkennen und Steuern der HEOS Geräte. Die class HeosPlayer dient der
* Interpretation der Antworten der Heos Geräte.
*
* Das Script muss lediglich gestartet werden, es sucht dann im Netzwerk nach HEOS Playern und
* erzeugt in der aktuellen Javascript-Instanz einen Subeintrag für die Favoriten und je einen Subeintrag
* für jeden gefundenen Player. Dort wiederum werden State-Variablen zur Aufnahme der Player-Daten angelegt.
*
* Das Script erzeugt auch on-Handler um auf Steuerungen über vis und andere Scripte an den State-Variablen
* reagieren zu können.
*
* Beim Beenden des Scripts werden alle Verbindungen und on-Handler geschlossen.
*
* Das Script ermöglicht die HEOS Player zu steuern, es soll aber nicht die HEOS App ersetzen. Dazu fehlen
* etliche Funktionen wie Playlisten-Handhabung, Gruppensteuerung usw.
*
*
* class Heos
****************************
* (a) Erkennen von HEOS Geräten (Playern)
* Das Erkennen der HEOS Geräte findet via UPD unter Nutzung von node-ssdp statt. node-ssdp muss dazu im
* Javascript-Adapter in der Konfiguration mit angegeben werden!
* (b) der Kommunikation über TelNet
* Es wird genau eine TelNet Verbindung zu einem HEOS Gerät aufgebaut, dieses reicht die Sendungen
* und Antworten zentral weiter
* (c) der Ermittlung von Favoriten (Presets) und
* Für jeden Favorit wird ein ioBroker State heos.presets.n mit entsprechenden Sub-States erzeugt.
* Zur Nutzung der Presets ist ein SignIn notwendig, hierzu müssen HEOS_USERNAME und HEOS_PASSWORD
* gesetzt werden.
* (d) dem Instanziieren der class HeosPlayer je HEOS Gerät.
* Je HEOS Gerät wird ein ioBroker-State mit der IP-Adresse des Players erzeugt, dieser State erhält
* diverse Sub-States
*
* State-Variable
* --------------
* .heos.command (write)
* Hierüber können der Heos-Klasse Befehle übergeben werden:
* connect : Verbindung zu HEOS aufbauen bzw. erneut aufbauen (praktisch ein reset)
* disconnect : Verbindung zu HEOS beenden
* load_presets : lädt die Favoriten neu
*
* Favoriten (je Favorit ein Unterordner n=1 bis Anzahl)
*
* .heos.presets.<n>.image_url (read)
* Bild des Favoriten
*
* .heos.presets.<n>.name (read)
* Name des Favoriten
*
* .heos.presets.<n>.playable (read)
* Spielbar? true/false
*
* .heos.presets.<n>.type (read)
* Typ des Favoriten (station, ...)
*
*
* class HeosPlayer
****************************
* (a) der Steuerung genau eines HEOS Gerätes. Hierzu wird die zentrale Telnet Verbindung der class Heos genutzt.
* (b) dem Erzeugen der ioBroker-States zum Speichern der Geräte-Werte
* (b) der Auswertung von Antworten der HEOS Geräte und Zuweisung an die entsprechenden ioBroker-States
*
* State-Variable
* --------------
* .heos.<player_ip>.command (write)
* Hierüber können dem Heos-Player Befehle übergeben werden. Diese werden im Klartext in die State-Variable
* geschrieben. Getrennt durch das | Zeichen können mehrere Befehle hintereinander eingetragen werden.
* Bsp: Setzen der Lautstärke auf 20 und Abspielen des 1.Favoriten
* set_volume&level=20|play_preset&preset=1
*
* set_volume&level=0|1|..|100 : Setzt die gewünschte Lautstärke
* set_play_state&state=play|pause|stop : Startet und stoppt die Wiedergabe
* set_play_mode&repeat=on_all|on_one|off&shuffle=on|off: Setzt Wiederholung und Zufallsweidergabe
* set_mute&state=on|off : Stumm schalten oder nicht
* volume_down&step=1..10 : Lautstärke verringern um
* volume_up&step=1..10 : Lauststäre erhöhen um
* play_next : Nächsten Titel spielen
* play_previous : Vorherigen Titel spielen
* play_preset&preset=1|2|..|n : Favorit Nr n abspielen
* play_stream&url=url_path : URL-Stream abspielen
*
* Befehle, die intern genutzt werden und nicht aufgerufen werden müssen:
*
* get_volume : Füllt den .heos.<player_ip>.volume State
* get_play_state : Füllt den .heos.<player_ip>.play_state State
* get_play_mode : Füllt den .heos.<player_ip>.play_mode State
* get_now_playing_media : Füllt die .heos.<player_ip>.now_playing_media_... States
*
*
* .heos.<player_ip>.cur_pos (read)
* lfd. Position der Wiedergabe in [Sek]
*
* .heos.<player_ip>.cur_pos_MMSS (read)
* lfd. Position der Wiedergabe im Format MM:SS
*
* .heos.<player_ip>.duration (read)
* Länge des aktuellen Titels in [Sek]
*
* .heos.<player_ip>.duration_MMSS (read)
* Länge des aktuellen Titels im Format MM:SS
*
* .heos.<player_ip>.ip (read)
* IP-Adresse des HEOS Players
*
* .heos.<player_ip>.last_error (read)
* Text des letzten Fehlers, der vom Player gesendet wurde. Wird bei jedem neuen Befehl zurückgesetzt.
* Wenn man bspw. versucht eine Wiedergabe über Amazon o.ä. zu starten und es gibt aber keine Verbindung
* dahin, steht hier der Fehlertext drin
*
* .heos.<player_ip>.model (read)
* Modell des HEOS Players
*
* .heos.<player_ip>.mute (read write)
* Boolsche Variable, die anzeigt ob der Player gemutet (Stumm geschaltet) wurde. Hierüber kann auch ein
* mute gesetzt werden
*
* .heos.<player_ip>.name (read)
* Name des HEOS Players
*
* .heos.<player_ip>.now_playing_media_... (read)
* Eine Gruppe von States, in welchen Infos über den aktuellen Titel gespeichert werden, wird automatisch
* aktualisiert.
*
* .heos.<player_ip>.pid (read)
* Player-ID des HEOS Players
*
* .heos.<player_ip>.play_mode_repeat (read write)
* Repeat-Modus der Wiedergabe. Mögliche Werte: on_all|on_one|off
*
* .heos.<player_ip>.play_mode_shuffle (read write)
* Zufallswiedergabe true/false
*
* .heos.<player_ip>.play_state (read write)
* Zustand des Players. Mögliche Werte play|pause|stop
*
* .heos.<player_ip>.serial (read)
* Seriennummmer des HEOS Players
*
* .heos.<player_ip>.volume (read write)
* Lautstärke im Bereich 0 - 100.
*
*
*
* Weiterführende Links
****************************
* HEOS CLI Protokoll: http://rn.dmglobal.com/euheos/HEOS_CLI_ProtocolSpecification.pdf
* http://forum.iobroker.net/viewtopic.php?f=30&t=5693&p=115554#p115554
*
**/
/****************************
* Konfiguration
****************************/
const HEOS_USERNAME = 'xxxx.';
const HEOS_PASSWORD = 'xxx';
const DEBUG = false;
/****************************
* ab hier nichts mehr ändern ;-)
****************************/
var net = require('net');
const stateDISCONNECTED = 0;
const stateCONNECTING = 1;
const stateCONNECTED = 2;
/********************
* class Heos
********************/
class Heos {
constructor() {
this.init();
createState( this.statePath+'command', '', {name: 'Kommando für Heos-Script' });
createState( this.statePath+'connected', false, {name: 'Verbunden?' });
on({id: this.statePath+'command', change: "any"}, (obj) => {
this.executeCommand( obj.state.val );
});
}
logDebug(msg) { if (DEBUG) console.log('[Heos] '+msg); }
log(msg) { console.log('[Heos] '+msg); }
logWarn(msg) { console.warn('[Heos] '+msg); }
logError(msg) { console.error('[Heos] '+msg); }
init() {
this.statePath = 'javascript.'+instance+'.heos.';
this.players = [];
this.net_client = undefined;
this.nodessdp_client = undefined;
this.ip='';
this.msgs = [];
this.lastResponse = '';
this.state = stateDISCONNECTED;
}
connect() { try {
setState( this.statePath+"connected", false );
const NodeSSDP = require('node-ssdp').Client;
this.nodessdp_client = new NodeSSDP();
this.nodessdp_client.on('response', (headers, statusCode, rinfo) => this.onNodeSSDPResponse(rinfo) );
this.nodessdp_client.on('error', error => { client.close(); this.logError(error); });
const searchTargetName = 'urn:schemas-denon-com:device:ACT-Denon:1';
this.nodessdp_client.search(searchTargetName);
} catch(err) { this.logError( 'onResponse: '+err.message ); } }
/** Alle Player stoppen und die TelNet Verbindung schließen
**/
disconnect() {
this.log('disconnecting from HEOS ...');
unsubscribe('javascript.1.heos');
if (typeof this.net_client!=='undefined') {
this.registerChangeEvents( false );
this.net_client.destroy();
this.net_client.unref();
}
setState( this.statePath+"connected", false );
this.log('disconnected from HEOS');
}
executeCommand(cmd) {
this.log('command: '+cmd);
switch (cmd) {
case 'load_presets' :
this.getMusicSources();
break;
case 'connect' :
this.disconnect();
this.init();
this.connect();
break;
case 'disconnect' :
this.disconnect();
break;
}
}
/** es wurde mindestens ein Player erkannt, nun über dessen IP alle bekannten HEOS Player
* durch senden von "player/get_players" ermitteln
*/
onNodeSSDPResponse(rinfo) { try {
// rinfo {"address":"192.168.2.225","family":"IPv4","port":53871,"size":430}
if (typeof this.net_client=='undefined') {
this.ip = rinfo.address;
this.log('connecting to '+this.ip+' ...');
this.net_client = net.connect({host:this.ip, port:1255});
this.net_client.setKeepAlive(true, 5000);
this.state = stateCONNECTING;
this.net_client.on('error',(error) => {
this.logError(error);
this.disconnect();
});
this.net_client.on('connect', () => {
setState( this.statePath+"connected", true );
this.state = stateCONNECTED;
this.log('connected to '+this.ip);
this.getPlayers();
this.registerChangeEvents( true );
this.signIn();
this.getMusicSources();
});
// Gegenseite hat die Verbindung geschlossen
this.net_client.on('end', () => {
this.logWarn('HEOS closed the connection to '+this.ip);
this.disconnect();
});
// timeout
this.net_client.on('timeout', () => {
this.logWarn('Timeout trying connect to '+this.ip);
this.disconnect();
});
// Datenempfang
this.net_client.on('data', (data) => this.onData(data) );
}
} catch(err) { this.logError( 'onNodeSSDPResponse: '+err.message ); } }
/** es liegen Antwort(en) vor
**/
onData(data) { try {
data = data.toString();
data=data.replace(/[\n\r]/g, ''); // Steuerzeichen "CR" entfernen
// es können auch mehrere Antworten vorhanden sein! {"heos": ... } {"heos": ... }
// diese nun in einzelne Antworten zerlegen
data=data.replace(/{"heos":/g, '|{"heos":');
var responses = data.split('|');
responses.shift();
for (var r=0; r<responses.length; r++ ) {
this.parseResponse(responses[r]);
}
// wenn weitere Msg zum Senden vorhanden sind, die nächste senden
if (this.msgs.length>0)
this.sendNextMsg();
} catch(err) { this.logError( 'onData: '+err.message ); } }
/** Antwort(en) verarbeiten. Sich wiederholende Antworten ignorieren
**/
parseResponse (response) { try {
if (response == this.lastResponse )
return
this.lastResponse = response;
var jmsg;
var i;
var jdata = JSON.parse(response);
if ( !jdata.hasOwnProperty('heos') || !jdata.heos.hasOwnProperty('command') || !jdata.heos.hasOwnProperty('message') )
return;
// msg auswerten
try {
jmsg = '{"' + decodeURI(jdata.heos.message).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g,'":"').replace(/\s/g,'_') + '"}';
jmsg = JSON.parse(jmsg);
} catch(err) {
jmsg = {};
}
this.logDebug('parseResponse: '+response);
// result ?
var result = 'success';
if (jdata.heos.hasOwnProperty('result') ) result = jdata.heos.result;
if ( result!='success' ) {
setState(this.statePath+'last_error', jmsg.text);
this.logWarn(jmsg.text);
}
// cmd auswerten
var cmd = jdata.heos.command.split('/');
var cmd_group = cmd[0];
cmd = cmd[1];
switch (cmd_group) {
case 'player':
switch (cmd) {
// {"heos": {"command": "player/get_players", "result": "success", "message": ""},
// "payload": [{"name": "HEOS Bar", "pid": 1262037998, "model": "HEOS Bar", "version": "1.430.160", "ip": "192.168.2.225", "network": "wifi", "lineout": 0, "serial": "ADAG9170202780"},
// {"name": "HEOS 1 rechts", "pid": -1746612370, "model": "HEOS 1", "version": "1.430.160", "ip": "192.168.2.201", "network": "wifi", "lineout": 0, "serial": "AMWG9170934429"},
// {"name": "HEOS 1 links", "pid": 68572158, "model": "HEOS 1", "version": "1.430.160", "ip": "192.168.2.219", "network": "wifi", "lineout": 0, "serial": "AMWG9170934433"}
// ]}
case 'get_players' :
if ( (jdata.hasOwnProperty('payload')) && (this.players.length===0) ) {
for (i=0; i<jdata.payload.length; i++) {
var player = jdata.payload[i];
this.players.push(player);
}
this.startPlayers();
}
break;
}
break;
// {"heos": {"command": "browse/get_music_sources", "result": "success", "message": ""},
// "payload": [{"name": "Amazon", "image_url": "https://production...png", "type": "music_service", "sid": 13},
// {"name": "TuneIn", "image_url": "https://production...png", "type": "music_service", "sid": 3},
// {"name": "Local Music", "image_url": "https://production...png", "type": "heos_server", "sid": 1024},
// {"name": "Playlists", "image_url": "https://production...png", "type": "heos_service", "sid": 1025},
// {"name": "History", "image_url": "https://production...png", "type": "heos_service", "sid": 1026},
// {"name": "AUX Input", "image_url": "https://production...png", "type": "heos_service", "sid": 1027},
// {"name": "Favorites", "image_url": "https://production...png", "type": "heos_service", "sid": 1028}]}
case 'browse':
switch (cmd) {
case 'get_music_sources' :
if ( (jdata.hasOwnProperty('payload')) ) {
for (i=0; i<jdata.payload.length; i++) {
var source = jdata.payload[i];
if (source.name=='Favorites') {
this.browse(source.sid);
}
}
}
break;
// {"heos": {"command": "browse/browse", "result": "success", "message": "pid=1262037998&sid=1028&returned=5&count=5"},
// "payload": [{"container": "no", "mid": "s17492", "type": "station", "playable": "yes", "name": "NDR 2 (Adult Contemporary Music)", "image_url": "http://cdn-radiotime-logos.tunein.com/s17492q.png"},
// {"container": "no", "mid": "s158432", "type": "station", "playable": "yes", "name": "Absolut relax (Easy Listening Music)", "image_url": "http://cdn-radiotime-logos.tunein.com/s158432q.png"},
// {"container": "no", "mid": "catalog/stations/A1W7U8U71CGE50/#chunk", "type": "station", "playable": "yes", "name": "Ed Sheeran", "image_url": "https://images-na.ssl-images-amazon.com/images/G/01/Gotham/DE_artist/EdSheeran._SX200_SY200_.jpg"},
// {"container": "no", "mid": "catalog/stations/A1O1J39JGVQ9U1/#chunk", "type": "station", "playable": "yes", "name": "Passenger", "image_url": "https://images-na.ssl-images-amazon.com/images/I/71DsYkU4QaL._SY500_CR150,0,488,488_SX200_SY200_.jpg"},
// {"container": "no", "mid": "catalog/stations/A316JYMKQTS45I/#chunk", "type": "station", "playable": "yes", "name": "Johannes Oerding", "image_url": "https://images-na.ssl-images-amazon.com/images/G/01/Gotham/DE_artist/JohannesOerding._SX200_SY200_.jpg"}],
// "options": [{"browse": [{"id": 20, "name": "Remove from HEOS Favorites"}]}]}
case 'browse' :
if ( (jdata.hasOwnProperty('payload')) ) {
for (i=0; i<jdata.payload.length; i++) {
var preset = jdata.payload[i];
createState( this.statePath+'presets.'+(i+1)+'.name', preset.name, {name: 'Favoritenname ' });
createState( this.statePath+'presets.'+(i+1)+'.playable', (preset.playable=='yes'?true:false), {name: 'Favorit ist spielbar' });
createState( this.statePath+'presets.'+(i+1)+'.type', preset.type, {name: 'Favorittyp' });
createState( this.statePath+'presets.'+(i+1)+'.image_url', preset.image_url, {name: 'Favoritbild' });
}
}
break;
}
break;
case 'system':
switch (cmd) {
case 'sign_in' :
this.log('signed in: '+jdata.heos.result);
break;
}
break;
}
// an die zugehörigen Player weiterleiten
if ( jmsg.hasOwnProperty('pid') ) {
for (i=0; i<this.players.length; i++)
if (jmsg.pid==this.players[i].heosPlayer.pid) {
this.players[i].heosPlayer.parseResponse (jdata, jmsg, cmd_group, cmd);
break;
}
}
} catch(err) { this.logError( 'parseResponse: '+err.message ); } }
/** sucht die zur ip passenden player-Insanz
**/
sendCommandToPlayer(objID, cmd ) {
var ip = objID;
ip = ip.split('.');
var ip_ = ip[3];
ip = ip_.replace(/_/g,'.');
for (var p=0; p<this.players.length; p++ )
if (this.players[p].ip == ip ) {
this.players[p].heosPlayer.sendCommand( cmd );
break;
}
}
/** Für die gefundenen HEOS Plyer entsprechende class HeosPlayer Instanzen bilden
**/
startPlayers() { try {
var i=0;
for (i=0; i<this.players.length; i++) {
this.players[i].heosPlayer = new HeosPlayer( this, this.players[i] );
}
// Events setzen
for (i=0; i<this.players.length; i++) {
var statePath = this.players[i].heosPlayer.statePath;
// on-Event für command
on({id: statePath+'command', change: "any"}, (obj) => {
this.sendCommandToPlayer( obj.id, obj.state.val );
});
// on-Event für volume (nur wenn ack=false, also über vis)
on({id: statePath+'volume', change:'ne', ack:false }, (obj) => {
this.sendCommandToPlayer( obj.id, 'set_volume&level='+obj.state.val );
});
// on-Event für mute (nur wenn ack=false, also über vis)
on({id: statePath+'mute', change: 'ne', ack:false}, (obj) => {
this.sendCommandToPlayer( obj.id, 'set_mute&state='+ (obj.state.val === true ? 'on' : 'off') );
});
// on-Event für play_mode_shuffle (nur wenn ack=false, also über vis)
on({id: statePath+'play_mode_shuffle', change: 'ne', ack:false}, (obj) => {
this.sendCommandToPlayer( obj.id, 'set_play_mode&shuffle='+ (obj.state.val === true ? 'on' : 'off') );
});
// on-Event für play_state (nur wenn ack=false, also über vis)
on({id: statePath+'play_state', change: 'ne', ack:false}, (obj) => {
this.sendCommandToPlayer( obj.id, 'set_play_state&state='+ obj.state.val );
});
}
} catch(err) { this.logError( 'startPlayers: '+err.message ); } }
getPlayers() {
if (this.state == stateCONNECTED) {
this.msgs.push( 'heos://player/get_players\n' );
this.sendNextMsg();
}
}
registerChangeEvents( b ) {
if (this.state == stateCONNECTED) {
if (b) this.msgs.push( 'heos://system/register_for_change_events?enable=on' );
else this.msgs.push( 'heos://system/register_for_change_events?enable=off' );
this.sendNextMsg();
}
}
signIn() {
if (this.state == stateCONNECTED) {
// heos://system/sign_in?un=heos_username&pw=heos_password
this.msgs.push( 'heos://system/sign_in?un='+HEOS_USERNAME+'&pw='+HEOS_PASSWORD );
this.sendNextMsg();
}
}
getMusicSources() {
if (this.state == stateCONNECTED) {
// heos://browse/get_music_sources
this.msgs.push( 'heos://browse/get_music_sources' );
this.sendNextMsg();
}
}
browse(sid) {
if (this.state == stateCONNECTED) {
// heos://browse/browse?sid=source_id
this.msgs.push( 'heos://browse/browse?sid='+sid );
this.sendNextMsg();
}
}
sendNextMsg () {
if (this.msgs.length>0) {
var msg = this.msgs.shift();
this.sendMsg(msg);
}
}
// Nachricht an player senden
sendMsg (msg) {
this.net_client.write(msg + "\n");
this.logDebug("data sent: "+ msg);
}
} // end of class Heos
/********************
* class HeosPlayer
********************/
class HeosPlayer {
constructor(heos, player) {
this.heos = heos;
this.ip = player.ip;
this.name = player.name;
this.pid = player.pid;
this.model = player.model;
this.serial = player.serial;
this.statePath = 'javascript.'+instance+'.heos.'+this.ip.replace(/\./g,'_')+".";
this.log('starting HEOS player for IP '+this.ip);
this.createStates();
// Initialiserung der States nach 5 Sek
setTimeout(() => {
this.setState('ip',this.ip);
this.setState('name',this.name);
this.setState('pid',this.pid);
this.setState('model',this.model);
this.setState('serial',this.serial);
this.sendCommand('get_play_state|get_play_mode|get_now_playing_media|get_volume');
}, 5000 );
}
static version () { return "0.1"; }
logDebug(msg) { if (DEBUG) console.log('[HeosPlayer '+this.ip+'] '+msg); }
log(msg) { console.log('[HeosPlayer '+this.ip+'] '+msg); }
logWarn(msg) { console.warn('[HeosPlayer '+this.ip+'] '+msg); }
logError(msg) { console.error('[HeosPlayer '+this.ip+'] '+msg); }
/** Anlage der ioBroker States für den Player
**/
createStates() {
const states = [
{ id:"command", name:"Kommando für HEOS Aufrufe"},
{ id:"ip", name:"IP Adresse"},
{ id:"pid", name:"Player-ID "},
{ id:"name", name:"Name des Players"},
{ id:"model", name:"Modell des Players"},
{ id:"serial", name:"Seriennummer des Players"},
{ id:"last_error", name:"Letzter Fehler"},
{ id:"volume", name:"Aktuelle Lautstärke"},
{ id:"mute", name:"Mute aktiviert?"},
{ id:"play_state", name:"Aktueller Wiedergabezustand"},
{ id:"play_mode_repeat", name:"Wiedergabewiederholung"},
{ id:"play_mode_shuffle", name:"Zufällige Wiedergabe"},
{ id:"now_playing_media_type", name:"Aktuelle Wiedergabemedium"},
{ id:"now_playing_media_song", name:"Aktuelles Lied"},
{ id:"now_playing_media_station", name:"Aktuelle Station"},
{ id:"now_playing_media_album", name:"Aktuelle Wiedergabemedium"},
{ id:"now_playing_media_artist", name:"Aktueller Artist"},
{ id:"now_playing_media_image_url", name:"Aktuelles Coverbild"},
{ id:"now_playing_media_album_id", name:"Aktuelle Album-ID"},
{ id:"now_playing_media_mid", name:"Aktuelle mid"},
{ id:"now_playing_media_qid", name:"Aktuelle qid"},
{ id:"cur_pos", name:"lfd. Position"},
{ id:"duration", name:"Dauer"},
{ id:"cur_pos_MMSS", name:"lfd. Position MM:SS"},
{ id:"duration_MMSS", name:"Dauer MM:SS"}
];
var def = "";
for (var s=0; s<states.length; s++) {
var state = states[s];
if (this.hasOwnProperty(state.id)) def=this[state.id]; else def='';
createState( this.statePath+state.id, def, {name: state.name });
}
}
/** wandelt einen sek Wert in MM:SS Darstellung um
**/
toMMSS (s) {
var sec_num = parseInt(s, 10);
var minutes = Math.floor(sec_num / 60);
var seconds = sec_num - (minutes * 60);
if (seconds < 10) {seconds = "0"+seconds;}
return minutes+':'+seconds;
}
/** setState wrapper
**/
setState(id,val) {
setState(this.statePath+id, val, true);
}
/** Auswertung der empfangenen Daten
**/
parseResponse (jdata, jmsg, cmd_group, cmd) { try {
switch (cmd_group) {
case 'event':
switch (cmd) {
case 'player_playback_error' :
this.setState('last_error', jmsg.error.replace(/_/g,' '));
this.logError(jmsg.error.replace(/_/g,' '));
break;
case 'player_state_changed' :
this.setState("play_state", jmsg.state);
break;
case 'player_volume_changed' :
this.setState("volume", jmsg.level );
break;
case 'player_mute_changed' :
this.setState("mute", (jmsg.state=='on' ? true : false) );
break;
case 'player_repeat_mode_changed' :
this.setState("play_mode_shuffle", jmsg.shuffle );
break;
case 'player_shuffle_mode_changed' :
this.setState("play_mode_repeat", jmsg.repeat );
break;
case 'player_now_playing_changed' :
this.sendCommand('get_now_playing_media');
break;
case 'player_now_playing_progress' :
this.setState("cur_pos", jmsg.cur_pos / 1000);
this.setState("cur_pos_MMSS", this.toMMSS(jmsg.cur_pos / 1000));
this.setState("duration", jmsg.duration / 1000);
this.setState("duration_MMSS", this.toMMSS(jmsg.duration / 1000));
break;
}
break;
case 'player':
switch (cmd) {
case 'set_volume' :
case 'get_volume' :
if ( getState(this.statePath+"volume").val != jmsg.level)
this.setState("volume", jmsg.level);
break;
case 'set_mute' :
case 'get_mute' :
this.setState("mute", (jmsg.state=='on' ? true : false) );
break;
case 'set_play_state' :
case 'get_play_state' :
this.setState("play_state", jmsg.state);
break;
case 'set_play_mode' :
case 'get_play_mode' :
this.setState("play_mode_repeat", jmsg.repeat);
this.setState("play_mode_shuffle", (jmsg.shuffle=='on'?true:false) );
break;
case 'get_now_playing_media' :
this.setState("now_playing_media_type", jdata.payload.type);
// type == station
if (jdata.payload.type=='station') {
this.setState("now_playing_media_song", jdata.payload.song);
this.setState("now_playing_media_station", jdata.payload.station);
this.setState("now_playing_media_album", jdata.payload.album);
this.setState("now_playing_media_artist", jdata.payload.artist);
this.setState("now_playing_media_image_url", jdata.payload.image_url);
this.setState("now_playing_media_album_id", jdata.payload.album_id);
this.setState("now_playing_media_mid", jdata.payload.mid);
this.setState("now_playing_media_qid", jdata.payload.qid);
}
break;
}
break;
} // switch
} catch(err) { this.logError( 'parseResponse: '+err.message ); } }
/** cmd der Form "cmd¶m" werden zur msg heos+cmd+pid+¶m aufbereitet
cmd der Form "cmd?param" werden zur msg heos+cmd+?param aufbereitet
**/
commandToMsg (cmd) {
var param = cmd.split('&');
cmd = param[0];
if ( param.length > 1 ) param='&'+param[1]; else param='';
var cmd_group = 'player';
switch (cmd) {
case 'get_play_state':
case 'get_play_mode':
case 'get_now_playing_media':
case 'get_volume':
case 'play_next':
case 'play_previous':
case 'set_mute': // &state=on|off
case 'set_volume': // &level=1..100
case 'volume_down': // &step=1..10
case 'volume_up': // &step=1..10
case 'set_play_state': // &state=play|pause|stop
case 'set_play_mode': // &repeat=on_all|on_one|off shuffle=on|off
break;
// browse
case 'play_preset': // heos://browse/play_preset?pid=player_id&preset=preset_position
cmd_group = 'browse';
break;
case 'play_stream': // heos://browse/play_stream?pid=player_id&url=url_path
cmd_group = 'browse';
break;
}
return 'heos://'+cmd_group+'/'+cmd+'?pid=' + this.pid + param;
}
/** Nachricht (command) an player senden
es sind auch mehrere commands, getrennt mit | erlaubt
bsp: set_volume&level=20|play_preset&preset=1
**/
sendCommand (command) {
this.setState('last_error', '');
var cmds = command.split('|');
for (var c=0; c<cmds.length; c++) {
this.heos.msgs.push( this.commandToMsg(cmds[c]) );
}
this.heos.sendNextMsg();
}
} // end of HeosPlayer
/* -----
Heos
----- */
// Heos Instanz erzeugen und verbinden
var heos = new Heos( );
heos.connect();
// wenn das Script beendet wird, dann auch die Heos Instanz beenden
onStop(function () {
heos.disconnect();
}, 0 );