NEWS
Wie mittels LUXTRONIK-Adapter Wassertemperatur einstellen?
-
Servus zusammen!
Ich benutze nun schon einige Jahre den Luxtronik 2 Adapter und habe nun ein Problem festgestellt.
Zur Einstellung der Brauchwassertemperatur nutze ich das Objekt luxtronik2.0.parameters.warmwater_temperature (lässt sich nur bis 65 Grad einstellen)
Nun habe ich aber festgestellt, dass die Alpha-Control-App fürs Iphone das Objekt luxtronik2.0.parameters.temperature_hot_water_target benutzt und die Reaktion der Wärmepumpe schnell auf den neu eingestellten Wert reagiert.
Per VIS mittels jiqui-SelectValueList, lässt sich das Objekt nicht steuern, da es die Werte nicht bestätigt. Selbst, wenn man es in den Objekten manuell einträgt und bestätigt, dann ändert sich der Wert nach kurzer Zeit wieder auf den zuvor eingestellten Wert.
Ich habe nun festgestellt, dass das Objekt warmwater_temperature irgendwie nicht so viel Prio hat als hot_water_target. In Verbindung mit der Warmwasser-Nachheizung mittels Heizstab, reagiert diese nur auf den hot_water_target Wert und nicht auf warmwater_temperature
Die Ansteuerung mittels hot_water_target wäre also unbedingt notwendig.Hat jemand eine Idee, wie ich das lösen kann?
-
@herrfrodo sagte in Wie mittels LUXTRONIK-Adapter Wassertemperatur einstellen?:
Selbst, wenn man es in den Objekten manuell einträgt uns bestätigt,
der Wert darf nicht bestätigt gesendet werden, wenn damit etwas gesteuert werden soll.
-
@homoran sagte in Wie mittels LUXTRONIK-Adapter Wassertemperatur einstellen?:
der Wert darf nicht bestätigt gesendet werden, wenn damit etwas gesteuert werden soll.
Auch bei Zahlen-Werten?
Habe gerade gesehen, dass das Objekt luxtronik2.0.parameters.temperature_hot_water_target schreibgeschützt ist.
-
@herrfrodo sagte in Wie mittels LUXTRONIK-Adapter Wassertemperatur einstellen?:
Auch bei Zahlen-Werten?
Natürlich!
sonst wird nur der Wert aktualisiert, aber keine Steuerfunktion durch den Adapter ausgelöst.@herrfrodo sagte in Wie mittels LUXTRONIK-Adapter Wassertemperatur einstellen?:
Habe gerade gesehen, dass das Objekt luxtronik2.0.parameters.temperature_hot_water_target schreibgeschützt ist.
ja, sieht man am abgeschlossebrn Schloss Icon in den Objekten
Jetzt ist die Frage ob das in der WP ein RO Wert ist, oder ob der Wert durch den Adapter ggf. irrtümlich in den Objekten schreibgeschützt wurde
-
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');