Hey zusammen,
ich habe ein Skript geschrieben, das die Steuerung der Wassertemperatur über MyUplink ermöglicht, da die Luxtronik Weboberfläche (ab Version 3.89.4) keine direkte Option dazu bietet. Der Plan ist, daraus später einen richtigen Adapter zu machen, aber hier ist schon einmal die Skriptversion zum Ausprobieren.
Was macht das Skript?
Die Luxtronik ab Version 3.89.4 kann MyUplink integrieren, und genau das nutzt dieses Skript aus. Das Skript erlaubt es euch, die Heizung und das Warmwasser über die MyUplink-Cloud zu steuern, und stellt alle Informationen zur Verfügung, die dort abgerufen werden können. Das bedeutet, ihr könnt die Temperatur direkt anpassen, ohne die Einschränkungen der normalen Weboberfläche. Mehr Informationen zu MyUplink findet ihr hier:
MyUplink Cloud – Alpha Innotec
So probiert ihr das Skript aus:
Öffnet den IoBroker > JavaScript Adapter > Neu > JavaScript > Hinzufügen.
Fügt das Skript ein.
Tragt eure MyUplink-Zugangsdaten ein.
Startet das Skript – und schon seid ihr einsatzbereit!
Funktionsumfang
Heizung & Warmwasser: Ihr könnt die gewünschten Temperaturen einstellen.
Informationsabruf: Alle relevanten Daten, die MyUplink zur Verfügung stellt, werden abgerufen und angezeigt.
Falls ihr das Skript ausprobiert, würde ich mich sehr über euer Feedback freuen. Viel Spaß damit!
// Konfigurationseinstellungen
const CONFIG = {
email: 'EMAIL@MYMAIL.DE',
password: 'GEHEIM',
clientId: 'My-Uplink-iOS',
clientSecret: '992EFE7C-9CDC-435B-8BA3-2EF8E81BEF14',
interval: 30000, // Standardintervall in Millisekunden für wiederkehrende Abfragen
intervalFunctions: [
'getWeatherTileData',
'getAndCreateMenu'
]
};
const axios = require('axios');
class MyUplinkAdapter {
constructor() {
this.token = null;
this.userData = {}; // Hier werden die Benutzerdaten gespeichert
this.devices = {}; // Hier werden die Gerätedaten gespeichert
this.groups = []; // Hier werden die Gruppendaten gespeichert
this.failedRequests = []; // Warteschlange für fehlgeschlagene Anfragen
log('Konstruktor von MyUplinkAdapter wurde aufgerufen!', 'info');
// Initialisieren des Verbindungsstatus
setObject('myUplink.info.connection', {
type: 'state',
common: {
name: 'API-Verbindungsstatus',
type: 'boolean',
role: 'indicator.connected',
read: true,
write: false
},
native: {}
});
setState('myUplink.info.connection', { val: false, ack: true });
}
// Authentifizierungsmethode
authenticate() {
log("Starte Authentifizierung...", 'info');
log(`Authentifizierungsparameter: email=${CONFIG.email}, clientId=${CONFIG.clientId}`, 'debug');
return axios.post('https://internalapi.myuplink.com/v2/users/validate', {
email: CONFIG.email,
password: CONFIG.password
}, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Language': 'de-DE'
}
})
.then(response => {
log("Benutzer validiert. Antwort: " + JSON.stringify(response.data), 'debug');
const validationData = response.data;
if (validationData.id) {
this.userData = validationData; // Speichert die Benutzerdaten
log(`Authentifizierung erfolgreich. Benutzer-ID: ${validationData.id}`, 'info');
setState('myUplink.info.connection', { val: true, ack: true }); // Verbindung erfolgreich
return this.getAccessToken(validationData.id);
} else {
log("Fehlende ID im Validierungsschritt. Antwort: " + JSON.stringify(validationData), 'error');
throw new Error('User validation failed: Missing ID');
}
})
.catch(error => {
this.handleError(error);
setState('myUplink.info.connection', { val: false, ack: true }); // Verbindung fehlgeschlagen
throw error;
});
}
// Methode zum Abrufen des Access Tokens
getAccessToken(userId) {
log("Abrufen des Access Tokens...", 'info');
log(`Tokenparameter: clientId=${CONFIG.clientId}, userId=${userId}`, 'debug');
return axios.post('https://internalapi.myuplink.com/oauth/token',
`password=${encodeURIComponent(CONFIG.password)}&client_secret=${CONFIG.clientSecret}&grant_type=password&client_id=${CONFIG.clientId}&username=${encodeURIComponent(CONFIG.email)}`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Authorization': `Bearer ${userId}`
}
}
)
.then(response => {
log("Token erfolgreich erhalten. Antwort: " + JSON.stringify(response.data), 'debug');
const tokenData = response.data;
if (tokenData.access_token) {
this.token = tokenData.access_token;
log(`Access-Token erfolgreich gespeichert.`, 'info');
return tokenData;
} else {
log("Fehlender Access-Token in der Antwort: " + JSON.stringify(tokenData), 'error');
throw new Error('Access token not received');
}
})
.catch(error => {
this.handleError(error);
throw error;
});
}
// Methode zum Speichern der Benutzerdaten
saveUserData() {
return this.ensureTokenIsValid().then(() => {
log("Speichern der Benutzerdaten...", 'info');
const userUrl = 'https://internalapi.myuplink.com/v2/users/me';
log(`Anfrage-URL für Benutzerdaten: ${userUrl}`, 'debug');
return axios.get(userUrl, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
}
})
.then(response => {
log("Benutzerdaten erfolgreich abgerufen. Antwort: " + JSON.stringify(response.data), 'debug');
this.userData = response.data;
// Dynamische Erstellung der Datenpunkte in myUplink.users und Setzen der Werte
Object.keys(this.userData).forEach(key => {
if (typeof this.userData[key] === 'object' && this.userData[key] !== null) {
Object.keys(this.userData[key]).forEach(subKey => {
const stateId = `myUplink.User.${key}.${subKey}`;
setObject(stateId, {
type: 'state',
common: {
name: `${key} ${subKey}`,
type: typeof this.userData[key][subKey],
read: true,
write: false
}
});
setState(stateId, {
val: this.userData[key][subKey],
ack: true
});
});
} else {
const stateId = `myUplink.User.${key}`;
setObject(stateId, {
type: 'state',
common: {
name: key,
type: typeof this.userData[key],
read: true,
write: false
}
});
setState(stateId, {
val: this.userData[key],
ack: true
});
}
});
return this.userData;
})
.catch(error => {
this.handleError(error);
throw error;
});
});
}
// Methode für das Abrufen der Wetterdaten
getWeatherTileData() {
return this.ensureTokenIsValid().then(() => {
const group = this.groups.find(g => g.id); // Nimmt die erste gültige Gruppe aus den gespeicherten Gruppendaten
if (!group || !group.id) {
log('Keine gültige Group ID gefunden.', 'error');
throw new Error('No valid Group ID found.');
}
const groupId = group.id;
const fetchWeatherData = () => {
log("Abrufen der Wetterdaten...", 'info');
const weatherUrl = `https://internalapi.myuplink.com/v2/groups/${groupId}/weather-tile-data`;
log(`Anfrage-URL für Wetterdaten: ${weatherUrl}`, 'debug');
return axios.get(weatherUrl, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
}
})
.then(response => {
log("Wetterdaten erfolgreich abgerufen. Antwort: " + JSON.stringify(response.data), 'debug');
const weatherData = response.data;
// Dynamische Erstellung der Datenpunkte in myUplink.Weather und Setzen der Werte
const basePath = `myUplink`;
Object.keys(weatherData).forEach(key => {
if (typeof weatherData[key] === 'object' && weatherData[key] !== null) {
if (Array.isArray(weatherData[key])) {
weatherData[key].forEach((item, index) => {
Object.keys(item).forEach(subKey => {
const stateId = `${basePath}.Weather.${key}[${index}].${subKey}`;
setObject(stateId, {
type: 'state',
common: {
name: `${key} ${subKey}`,
type: typeof item[subKey],
read: true,
write: false
}
});
setState(stateId, {
val: item[subKey],
ack: true
});
});
});
} else {
Object.keys(weatherData[key]).forEach(subKey => {
const stateId = `${basePath}.Weather.${key}.${subKey}`;
setObject(stateId, {
type: 'state',
common: {
name: `${key} ${subKey}`,
type: typeof weatherData[key][subKey],
read: true,
write: false
}
});
setState(stateId, {
val: weatherData[key][subKey],
ack: true
});
});
}
} else {
const stateId = `${basePath}.Weather.${key}`;
setObject(stateId, {
type: 'state',
common: {
name: key,
type: typeof weatherData[key],
read: true,
write: false
}
});
setState(stateId, {
val: weatherData[key],
ack: true
});
}
});
return weatherData;
})
.catch(error => {
this.handleError(error, fetchWeatherData);
});
};
// Erstmaliger Aufruf und Intervall einrichten
fetchWeatherData();
setInterval(fetchWeatherData, CONFIG.interval);
});
}
// Methode zum Sicherstellen, dass der Token noch gültig ist
ensureTokenIsValid() {
if (!this.token) {
log("Token ist nicht vorhanden oder ungültig. Hole neuen Token...", 'warn');
return this.authenticate();
}
return Promise.resolve();
}
// Methode zum Abrufen der Gruppendaten
getGroups() {
return this.ensureTokenIsValid().then(() => {
log("Abrufen der Gruppendaten...", 'info');
const groupsUrl = 'https://internalapi.myuplink.com/v2/groups/me';
log(`Anfrage-URL für Gruppendaten: ${groupsUrl}`, 'debug');
return axios.get(groupsUrl, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
}
})
.then(response => {
log("Gruppendaten erfolgreich abgerufen. Antwort: " + JSON.stringify(response.data), 'debug');
this.groups = response.data.groups || []; // Speichert die Gruppendaten
this.devices = this.groups.flatMap(group => group.devices) || []; // Speichert die Gerätedaten
this.createObjectsFromGroupsAndDevices();
return this.groups;
})
.catch(error => {
this.handleError(error);
throw error;
});
});
}
// Methode zum Erstellen der Baumstruktur in myUplink
createObjectsFromGroupsAndDevices() {
log("Erstelle Objekte für Gruppen und Geräte...", 'info');
this.groups.forEach(group => {
const groupId = group.id;
group.devices.forEach(device => {
const deviceId = device.id;
const groupPath = `myUplink.System.${groupId}.${deviceId}`;
// Erstellt das Geräteobjekt innerhalb der Gruppe
setObject(groupPath, {
type: 'device',
common: {
name: device.name || 'Unbenanntes Gerät',
desc: device.description || ''
},
native: device
});
});
});
log("Objekterstellung abgeschlossen.", 'info');
}
// Methode zum Abrufen der Menüstruktur
async getAndCreateMenu(menuId = 0, basePath = `myUplink`) {
try {
const device = this.groups.flatMap(group => group.devices).find(device => device.id);
if (!device || !device.id) {
log('Keine gültige Device ID gefunden.', 'error');
throw new Error('No valid Device ID found.');
}
const menuUrl = `https://internalapi.myuplink.com/v3/devices/${device.id}/menu/${menuId}`;
const response = await axios.get(menuUrl, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
}
});
const menuData = response.data;
if (menuData.rows && menuData.rows.length > 0) {
for (const row of menuData.rows) {
let name = row.text?.text || `Menü_${row.id}`;
name = this.normalizeName(name); // Normalisieren von Umlauten
const path = `${basePath}.${name}`;
// Falls es ein weiterer Menü-Link ist, rufe das nächste Menü ab
if (row.type === 'uilink') {
setObject(path, {
type: 'channel',
common: {
name: name,
desc: `Menü ID: ${row.id}`
},
native: row
});
await this.getAndCreateMenu(row.id, path);
}
// Falls es ein Parameter ist, erstelle das Parameter-Objekt
if (row.type === 'uiinfoparameter' || row.type === 'uinumerical' || row.type === 'uidropdown' || row.type === 'uiboolean') {
this.createParameterObject(path, row);
}
}
}
} catch (error) {
this.handleError(error);
}
}
// Methode zum Erstellen eines Parameter-Objekts
createParameterObject(path, parameter) {
const parameterName = parameter.text?.text || `Parameter_${parameter.parameterId}`;
let value = parameter.value?.integerValue ?? parameter.value?.stringValue ?? null;
let adjustedValue = value;
// Divisor anwenden, falls vorhanden und nur auf den Wert im Objekt selbst
if (parameter.metadata?.divisor) {
if (typeof value === 'number') {
adjustedValue = value / parameter.metadata.divisor;
}
}
// Wert als Text anzeigen, falls enumValues vorhanden sind
const states = {};
if (parameter.metadata?.enumValues && parameter.metadata.enumValues.length > 0) {
parameter.metadata.enumValues.forEach(ev => {
states[ev.value] = ev.text;
});
adjustedValue = value;
}
// Setze den Objekt-Typ je nach Parameter-Typ
const commonType = parameter.metadata?.variableType?.toLowerCase() || 'mixed';
const role = parameter.type === 'uiboolean' ? 'switch' : 'text';
setObject(path, {
type: 'state',
common: {
name: parameterName,
type: commonType,
unit: parameter.metadata?.unit || '',
read: true,
write: parameter.metadata?.isWritable || false,
role: role,
states: Object.keys(states).length > 0 ? states : undefined,
min: parameter.metadata?.minValue && parameter.metadata.minValue !== 0 ? parameter.metadata.minValue / (parameter.metadata.divisor || 1) : undefined,
max: parameter.metadata?.maxValue && parameter.metadata.maxValue !== 0 ? parameter.metadata.maxValue / (parameter.metadata.divisor || 1) : undefined,
step: parameter.metadata?.change ? parameter.metadata.change / (parameter.metadata.divisor || 1) : undefined
},
native: parameter
});
// Setze den Wert des Parameters als Zahl oder Enum-Wert
setState(path, {
val: typeof adjustedValue === 'number' ? adjustedValue : value,
ack: true
});
// Lausche auf Änderungen des Werts, um die PUT-Anfrage zu senden
if (parameter.metadata?.isWritable) {
on({id: path, change: 'ne'}, (obj) => { // 'ne' bedeutet, nur nicht bestätigte Änderungen lauschen
if (obj.state.ack) {
return; // Wenn die Änderung von der API bestätigt wurde, ignoriere sie
}
let newValue = obj.state.val;
const divisor = parameter.metadata?.divisor || 1;
const minValue = typeof parameter.metadata?.minValue !== 'undefined' && parameter.metadata.minValue !== 0 ? parameter.metadata.minValue / divisor : undefined;
const maxValue = typeof parameter.metadata?.maxValue !== 'undefined' && parameter.metadata.maxValue !== 0 ? parameter.metadata.maxValue / divisor : undefined;
const change = parameter.metadata?.change ? parameter.metadata.change / divisor : undefined;
// Werte begrenzen und anpassen
if (typeof minValue !== 'undefined' && newValue < minValue) {
newValue = minValue;
}
if (typeof maxValue !== 'undefined' && newValue > maxValue) {
newValue = maxValue;
}
if (typeof change !== 'undefined') {
newValue = Math.round(newValue / change) * change;
}
const adjustedValueForPut = newValue * divisor;
let currentPath = path;
let menuId = null;
while (!menuId && currentPath.includes('.')) {
currentPath = currentPath.substring(0, currentPath.lastIndexOf('.'));
const currentObject = getObject(currentPath);
if (currentObject?.native?.id) {
menuId = currentObject.native.id;
}
}
if (menuId) {
this.updateParameterValue(menuId, parameter.parameterId, adjustedValueForPut, parameter.metadata?.unit || '', path, newValue);
} else {
log(`Fehler: Menü-ID für Pfad ${path} nicht gefunden.`, 'error');
}
});
}
}
// Methode zum Normalisieren von Umlauten
normalizeName(name) {
return name
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/[^a-zA-Z0-9]/g, '_');
}
// Methode zur Aktualisierung der Parameterwerte (PUT-Call)
updateParameterValue(menuId, parameterId, value, unit = "", path, newValue) {
const device = this.groups.flatMap(group => group.devices).find(device => device.id);
if (!device || !device.id) {
log('Keine gültige Device ID gefunden.', 'error');
throw new Error('No valid Device ID found.');
}
const updateUrl = `https://internalapi.myuplink.com/v2/devices/${device.id}/menu/${menuId}/rawpoints/${parameterId}`;
log(`Sende PUT-Anfrage an URL: ${updateUrl} mit Wert: ${value} und Einheit: ${unit}`, 'info');
return axios.put(updateUrl, {
value: value,
unit: unit
}, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
})
.then(response => {
log(`Parameter erfolgreich aktualisiert: ${JSON.stringify(response.data)}`, 'debug');
setState(path, {
val: newValue,
ack: true
}); // Bestätigt den Wert im Objekt
})
.catch(error => {
this.handleError(error, () => this.updateParameterValue(menuId, parameterId, value, unit, path, newValue));
});
}
// Fehlerbehandlungs-Methode
handleError(error, retryCallback = null) {
if (error.response) {
log('Fehler-Antwortdaten: ' + JSON.stringify(error.response.data, null, 2), 'error');
log('HTTP-Statuscode: ' + error.response.status, 'error');
if (error.response.status === 401) {
log('Token abgelaufen, erneuere den Token...', 'warn');
this.authenticate().then(() => {
if (retryCallback) {
retryCallback();
}
});
}
}
setState('myUplink.info.connection', { val: false, ack: true }); // Verbindung fehlgeschlagen
}
}
// Beispiel für die Verwendung der MyUplinkAdapter-Klasse (alle Logs als Fehler oder Warnungen)
log("Starte die Authentifizierung...", 'info');
const adapter = new MyUplinkAdapter();
adapter.authenticate()
.then(() => adapter.getGroups())
.then(groups => {
log("Gruppendaten empfangen: " + JSON.stringify(groups), 'debug');
log("Gerätedaten empfangen: " + JSON.stringify(adapter.devices), 'debug');
return adapter.saveUserData();
})
.then(userData => {
log("Benutzerdaten gespeichert: " + JSON.stringify(userData), 'info');
return adapter.getWeatherTileData();
})
.then(weatherData => {
log("Wetterdaten empfangen: " + JSON.stringify(weatherData), 'info');
})
.then(() => adapter.getAndCreateMenu())
.then(() => {
log('Menüstruktur erfolgreich erstellt.', 'info');
})
.then(() => {
// Dynamisches Registrieren der Intervall-Funktionen
CONFIG.intervalFunctions.forEach(funcName => {
if (typeof adapter[funcName] === 'function') {
setInterval(() => {
adapter[funcName]();
}, CONFIG.interval);
log(`${funcName} wurde als Intervallfunktion registriert.`, 'info');
} else {
log(`Funktion ${funcName} existiert nicht im Adapter.`, 'error');
}
});
})
.catch(err => {
log('Allgemeiner Fehler: ' + (err.message || "Keine Fehlermeldung verfügbar"), 'error');
});
log("Ende des Skripts erreicht.", 'info');