Zeilenenden vereinheitlichen: LF ueberall ausser im Fremdcode
Bisher entschied jede Arbeitsstation fuer sich, welche Zeilenenden ein Commit bekommt: Git fuer Windows setzt core.autocrlf=input in seiner System-Konfiguration, die NAS setzt nichts. Weil der Bestand ausserdem in sich gemischt war - 34 PHP-Dateien mit LF, 30 mit CRLF -, gab es gar kein richtiges Zeilenende, an das ein Werkzeug sich haette halten koennen. Jede Ergaenzung mit LF in einer CRLF-Datei ergab eine gemischte Datei; helper.php und js/solar/homeMQTT.js waren bereits so entstanden. .gitattributes macht die Zeilenenden zur Eigenschaft des Repositorys. LF, weil dieses Verzeichnis der Web-Ordner der NAS ist und von Linux ausgeliefert wird - und weil Git fuer Windows mit "input" ohnehin schon dorthin zeigt. restricted/WebAuthn bleibt ausgenommen und damit byteweise so, wie die Bibliothek geliefert wurde; darunter liegen 292 Zertifikate. Von den 47 geaenderten Dateien wurde nachgerechnet keine einzige im Inhalt angefasst - der Vergleich HEAD-ohne-CR gegen Index ist ueberall gleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+6
-6
File diff suppressed because one or more lines are too long
+148
-148
@@ -1,149 +1,149 @@
|
||||
async function checkRegistration() {
|
||||
try {
|
||||
|
||||
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
|
||||
throw new Error('Browser not supported.');
|
||||
}
|
||||
|
||||
// get check args
|
||||
let rep = await window.fetch('authServer.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'});
|
||||
const getArgs = await rep.json();
|
||||
|
||||
// error handling
|
||||
if (getArgs.success === false) {
|
||||
throw new Error(getArgs.msg);
|
||||
}
|
||||
|
||||
// replace binary base64 data with ArrayBuffer. a other way to do this
|
||||
// is the reviver function of JSON.parse()
|
||||
recursiveBase64StrToArrayBuffer(getArgs);
|
||||
|
||||
// check credentials with hardware
|
||||
const cred = await navigator.credentials.get(getArgs);
|
||||
|
||||
// create object for transmission to server
|
||||
const authenticatorAttestationResponse = {
|
||||
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
|
||||
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
|
||||
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
|
||||
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
|
||||
};
|
||||
|
||||
// send to server
|
||||
rep = await window.fetch('authServer.php?fn=processGet' + getGetParams(), {
|
||||
method:'POST',
|
||||
body: JSON.stringify(authenticatorAttestationResponse),
|
||||
cache:'no-cache'
|
||||
});
|
||||
const authenticatorAttestationServerResponse = await rep.json();
|
||||
|
||||
// check server response
|
||||
if (authenticatorAttestationServerResponse.success) {
|
||||
window.location.reload();
|
||||
/// window.alert(authenticatorAttestationServerResponse.msg || 'login success');
|
||||
|
||||
} else {
|
||||
throw new Error(authenticatorAttestationServerResponse.msg);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
//reloadServerPreview();
|
||||
window.alert(err.message || 'unknown error occured');
|
||||
}
|
||||
}
|
||||
|
||||
function queryFidoMetaDataService() {
|
||||
window.fetch('authServer.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||
return response.json();
|
||||
|
||||
}).then(function(json) {
|
||||
if (json.success) {
|
||||
window.alert(json.msg);
|
||||
} else {
|
||||
throw new Error(json.msg);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
window.alert(err.message || 'unknown error occured');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert RFC 1342-like base64 strings to array buffer
|
||||
* @param {mixed} obj
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function recursiveBase64StrToArrayBuffer(obj) {
|
||||
let prefix = '=?BINARY?B?';
|
||||
let suffix = '?=';
|
||||
if (typeof obj === 'object') {
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
let str = obj[key];
|
||||
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||
str = str.substring(prefix.length, str.length - suffix.length);
|
||||
|
||||
let binary_string = window.atob(str);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
obj[key] = bytes.buffer;
|
||||
}
|
||||
} else {
|
||||
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ArrayBuffer to Base64
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {String}
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL parameter
|
||||
* @returns {String}
|
||||
*/
|
||||
function getGetParams() {
|
||||
let url = '';
|
||||
|
||||
url += '&apple=1';
|
||||
url += '&yubico=1';
|
||||
url += '&solo=1';
|
||||
url += '&hypersecu=1';
|
||||
url += '&google=1';
|
||||
url += 'µsoft=1';
|
||||
url += '&mds=1';
|
||||
|
||||
url += '&requireResidentKey=0';
|
||||
|
||||
url += '&type_usb=1';
|
||||
url += '&type_nfc=1';
|
||||
url += '&type_ble=1';
|
||||
url += '&type_int=1';
|
||||
url += '&type_hybrid=1';
|
||||
|
||||
url += '&fmt_android-key=1';
|
||||
url += '&fmt_android-safetynet=1';
|
||||
url += '&fmt_apple=1';
|
||||
url += '&fmt_fido-u2f=1';
|
||||
url += '&fmt_none=0' ;
|
||||
url += '&fmt_packed=1';
|
||||
url += '&fmt_tpm=1';
|
||||
url += '&userVerification=discouraged';
|
||||
|
||||
return url;
|
||||
async function checkRegistration() {
|
||||
try {
|
||||
|
||||
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
|
||||
throw new Error('Browser not supported.');
|
||||
}
|
||||
|
||||
// get check args
|
||||
let rep = await window.fetch('authServer.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'});
|
||||
const getArgs = await rep.json();
|
||||
|
||||
// error handling
|
||||
if (getArgs.success === false) {
|
||||
throw new Error(getArgs.msg);
|
||||
}
|
||||
|
||||
// replace binary base64 data with ArrayBuffer. a other way to do this
|
||||
// is the reviver function of JSON.parse()
|
||||
recursiveBase64StrToArrayBuffer(getArgs);
|
||||
|
||||
// check credentials with hardware
|
||||
const cred = await navigator.credentials.get(getArgs);
|
||||
|
||||
// create object for transmission to server
|
||||
const authenticatorAttestationResponse = {
|
||||
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
|
||||
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
|
||||
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
|
||||
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
|
||||
};
|
||||
|
||||
// send to server
|
||||
rep = await window.fetch('authServer.php?fn=processGet' + getGetParams(), {
|
||||
method:'POST',
|
||||
body: JSON.stringify(authenticatorAttestationResponse),
|
||||
cache:'no-cache'
|
||||
});
|
||||
const authenticatorAttestationServerResponse = await rep.json();
|
||||
|
||||
// check server response
|
||||
if (authenticatorAttestationServerResponse.success) {
|
||||
window.location.reload();
|
||||
/// window.alert(authenticatorAttestationServerResponse.msg || 'login success');
|
||||
|
||||
} else {
|
||||
throw new Error(authenticatorAttestationServerResponse.msg);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
//reloadServerPreview();
|
||||
window.alert(err.message || 'unknown error occured');
|
||||
}
|
||||
}
|
||||
|
||||
function queryFidoMetaDataService() {
|
||||
window.fetch('authServer.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||
return response.json();
|
||||
|
||||
}).then(function(json) {
|
||||
if (json.success) {
|
||||
window.alert(json.msg);
|
||||
} else {
|
||||
throw new Error(json.msg);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
window.alert(err.message || 'unknown error occured');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert RFC 1342-like base64 strings to array buffer
|
||||
* @param {mixed} obj
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function recursiveBase64StrToArrayBuffer(obj) {
|
||||
let prefix = '=?BINARY?B?';
|
||||
let suffix = '?=';
|
||||
if (typeof obj === 'object') {
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
let str = obj[key];
|
||||
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||
str = str.substring(prefix.length, str.length - suffix.length);
|
||||
|
||||
let binary_string = window.atob(str);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
obj[key] = bytes.buffer;
|
||||
}
|
||||
} else {
|
||||
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ArrayBuffer to Base64
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {String}
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL parameter
|
||||
* @returns {String}
|
||||
*/
|
||||
function getGetParams() {
|
||||
let url = '';
|
||||
|
||||
url += '&apple=1';
|
||||
url += '&yubico=1';
|
||||
url += '&solo=1';
|
||||
url += '&hypersecu=1';
|
||||
url += '&google=1';
|
||||
url += 'µsoft=1';
|
||||
url += '&mds=1';
|
||||
|
||||
url += '&requireResidentKey=0';
|
||||
|
||||
url += '&type_usb=1';
|
||||
url += '&type_nfc=1';
|
||||
url += '&type_ble=1';
|
||||
url += '&type_int=1';
|
||||
url += '&type_hybrid=1';
|
||||
|
||||
url += '&fmt_android-key=1';
|
||||
url += '&fmt_android-safetynet=1';
|
||||
url += '&fmt_apple=1';
|
||||
url += '&fmt_fido-u2f=1';
|
||||
url += '&fmt_none=0' ;
|
||||
url += '&fmt_packed=1';
|
||||
url += '&fmt_tpm=1';
|
||||
url += '&userVerification=discouraged';
|
||||
|
||||
return url;
|
||||
}
|
||||
Vendored
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+13
-13
File diff suppressed because one or more lines are too long
Vendored
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
/*! https://github.com/FranBar1966/bootstrap-5-modal-dynamic - License in the terms described in the LICENSE file */
|
||||
/*! https://github.com/FranBar1966/bootstrap-5-modal-dynamic - License in the terms described in the LICENSE file */
|
||||
function startDynamicModal(){document.body.removeEventListener("click",dynamicModalHandler),document.body.addEventListener("click",dynamicModalHandler)}function dynamicModalHandler(e){const t=e.target.closest(".modal-dynamic");if(!t)return;e.target.closest("a")&&e.preventDefault();const a=e.target.getAttribute("href"),d=e.target.dataset.template||"#modalTemplate";let o=document.querySelector(a);if(!o){const e=document.querySelector(d);e&&(o=e.cloneNode(!0),o.id=a.substring(1),document.body.appendChild(o))}if(!o)return;const n=e.target.dataset.class,r=e.target.dataset.title,s=e.target.dataset.header,l=e.target.dataset.noheader,c=e.target.dataset.url,i=e.target.dataset.footer,u=e.target.dataset.nofooter,m=e.target.dataset.width||"",y=e.target.dataset.backdrop||"false",h=!e.target.dataset.keyboard||"true"===e.target.dataset.keyboard;if(n&&o.classList.add(...n.split(" ")),s){const e=o.querySelector(".modal-header"),t=document.querySelector(s);e&&t&&(e.innerHTML=t.innerHTML)}if(l){const e=o.querySelector(".modal-header");e&&e.classList.add("hidden","d-none")}if(r){const e=o.querySelector(".modal-title");e&&(e.innerHTML=r)}if(i){const e=o.querySelector(".modal-footer"),t=document.querySelector(i);e&&t&&(e.innerHTML=t.innerHTML)}if(u){const e=o.querySelector(".modal-footer");e&&e.classList.add("hidden","d-none")}if(m){const e=o.querySelector(".modal-dialog");if(e){const t=isNaN(m)||""===m?m:m+"px";e.style.maxWidth=t,e.style.width="auto"}}let f=bootstrap.Modal.getInstance(o);f||(f=new bootstrap.Modal(o,{keyboard:h,backdrop:y})),o.addEventListener("hidden.bs.modal",e=>{o.remove()}),o.addEventListener("shown.bs.modal",e=>{o.focus()}),f.show();const g=o.querySelector(".modal-body");if(c.startsWith("#")){const e=document.querySelector(c);g.innerHTML=e?e.innerHTML:"ERROR: Content not found"}else fetch(c,{method:"GET",headers:{"X-Requested-From-Modal":a.substring(1),"Requested-With-Ajax":"ajax"}}).then(e=>e.text()).then(e=>{g.innerHTML=e,window.dispatchEvent(new CustomEvent("neutralFetchCompleted",{detail:{element:o,url:c}}))}).catch(e=>{g.innerHTML=e.message,window.dispatchEvent(new CustomEvent("neutralFetchError",{detail:{element:o,url:c}}))})}startDynamicModal(),window.addEventListener("neutralFetchCompleted",()=>{startDynamicModal()});
|
||||
Vendored
+6
-6
File diff suppressed because one or more lines are too long
+859
-859
File diff suppressed because it is too large
Load Diff
+193
-193
@@ -1,193 +1,193 @@
|
||||
var mqttData = {};
|
||||
|
||||
const solarMQTT = {
|
||||
getMQTT: function () {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const topic = "#";
|
||||
const connection = "wss://mqtt.nas.el-wa.org:443"
|
||||
mqttsolarTreeDone = false;
|
||||
// const connection = "ws://username:password@37.97.203.138:8083" // Works
|
||||
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
|
||||
const client = mqtt.connect(connection, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
client.on("message", messageReceived);
|
||||
client.on("connect", function () {
|
||||
client.subscribe("solarManager/#");
|
||||
client.subscribe("wattpilot/properties/lmo/state");
|
||||
client.subscribe("wattpilot/properties/ftt/state");
|
||||
client.subscribe("wattpilot/properties/fte/state");
|
||||
client.subscribe("wattpilot/properties/amp/state");
|
||||
client.subscribe("wattpilot/properties/car/state");
|
||||
client.subscribe("go-eCharger/270003/amp");
|
||||
client.subscribe("go-eCharger/270003/ate");
|
||||
client.subscribe("go-eCharger/270003/lmo");
|
||||
client.subscribe("go-eCharger/270003/att");
|
||||
client.subscribe("go-eCharger/270003/car");
|
||||
client.subscribe("weatherStation/#");
|
||||
});
|
||||
client.on("error", function (error) {
|
||||
//alert("MQTT Error: " + error);
|
||||
});
|
||||
client.on('end', function () {
|
||||
setTimeout(getMQTT, 5000);
|
||||
alert("MQTT Disconnected, try to reconnect in 5 secs.");
|
||||
})
|
||||
|
||||
function getNestedProp(obj, path) {
|
||||
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
|
||||
}
|
||||
function setNestedProp(obj, path, value) {
|
||||
var schema = obj; // a moving reference to internal objects within obj
|
||||
var pList = path.split('/');
|
||||
var len = pList.length;
|
||||
for (var i = 0; i < len - 1; i++) {
|
||||
var elem = pList[i];
|
||||
if (!schema[elem]) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[pList[len - 1]] = value;
|
||||
}
|
||||
|
||||
function messageReceived(topic, message) {
|
||||
setNestedProp(mqttData, topic, message);
|
||||
if (topic == "solarManager/P_Load") {
|
||||
setTimeout(function () { solarSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
|
||||
} else if (topic == "weatherStation/windDeg") {
|
||||
setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation"]?.[name]) }, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const solarSVG = {
|
||||
updateCnt: 99,
|
||||
updateValuesMQTT: function (mqttData) {
|
||||
if (this.updateCnt > 10) {
|
||||
this.updateCnt = 0;
|
||||
var obj = document.querySelector("object");
|
||||
var htmlNode = obj.contentDocument;
|
||||
htmlNode.getElementById("PufferOtxt").innerHTML = mqttData["solarManager"]["t_buffT"] + " °C";
|
||||
htmlNode.getElementById("PufferMtxt").innerHTML = mqttData["solarManager"]["t_buffM"] + " °C";
|
||||
htmlNode.getElementById("PufferUtxt").innerHTML = mqttData["solarManager"]["t_buffB"] + " °C";
|
||||
htmlNode.getElementById("heaterVL").innerHTML = mqttData["solarManager"]["t_heatVL"] + " °C";
|
||||
htmlNode.getElementById("heaterRL").innerHTML = mqttData["solarManager"]["t_heatRL"] + " °C";
|
||||
htmlNode.getElementById("thermeVLfb").innerHTML = mqttData["solarManager"]["t_gasVLu"] + " °C";
|
||||
htmlNode.getElementById("thermeVLww").innerHTML = mqttData["solarManager"]["t_gasVLo"] + " °C";
|
||||
htmlNode.getElementById("thermeRL").innerHTML = mqttData["solarManager"]["t_gasRL"] + " °C";
|
||||
htmlNode.getElementById("fbVL").innerHTML = mqttData["solarManager"]["t_fbVL"] + " °C";
|
||||
htmlNode.getElementById("fbRL").innerHTML = mqttData["solarManager"]["t_fbRL"] + " °C";
|
||||
htmlNode.getElementById("triac").innerHTML = mqttData["solarManager"]["t_triac"] + " °C";
|
||||
}
|
||||
this.updateCnt++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var chartSettings = {
|
||||
type: 'line',
|
||||
options: {
|
||||
animation: true,
|
||||
plugins: {
|
||||
annotation: {
|
||||
common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true },
|
||||
annotations: []
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
pointStyle: "circle",
|
||||
boxWidth: 4,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.dataset.yAxisID == "y1") {
|
||||
label += Math.round(context.parsed.y * 10) / 10 + " " + "L/min";
|
||||
} else {
|
||||
if (context.parsed.y !== null) {
|
||||
ret = scale(Math.round(context.parsed.y), false);
|
||||
label += ret[0] + " " + ret[1] + "°C";
|
||||
}
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
pointStyleWidth: 10,
|
||||
usePointStyle: true,
|
||||
pointStyle: "line",
|
||||
}
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
adapters: {
|
||||
date: {
|
||||
locale: "DE-de"
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
|
||||
},
|
||||
type: 'timestack',
|
||||
},
|
||||
y: {
|
||||
stacked: false,
|
||||
display: true,
|
||||
position: 'left',
|
||||
ticks: {
|
||||
callback: value => `${value} °C`,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Temperatur"
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
stacked: false,
|
||||
display: true,
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: value => `${value} L/min`,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Wasserverbrauch"
|
||||
},
|
||||
data:{}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var chartData = {};
|
||||
const heatChart = new Chart(
|
||||
document.querySelector('#heat-chart'),
|
||||
Object.assign({}, chartSettings)
|
||||
);
|
||||
const waterChart = new Chart(
|
||||
document.querySelector('#water-chart'),
|
||||
Object.assign({}, chartSettings)
|
||||
);
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
solarMQTT.getMQTT();
|
||||
getData(heatChart, 'ajax/getHeaterData.php', { sunrise: { FROM: -24, TO: 0 } });
|
||||
getData(waterChart, 'ajax/getWaterData.php', { sunrise: { FROM: -24, TO: 0 } });
|
||||
}
|
||||
});
|
||||
var mqttData = {};
|
||||
|
||||
const solarMQTT = {
|
||||
getMQTT: function () {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const topic = "#";
|
||||
const connection = "wss://mqtt.nas.el-wa.org:443"
|
||||
mqttsolarTreeDone = false;
|
||||
// const connection = "ws://username:password@37.97.203.138:8083" // Works
|
||||
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
|
||||
const client = mqtt.connect(connection, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
client.on("message", messageReceived);
|
||||
client.on("connect", function () {
|
||||
client.subscribe("solarManager/#");
|
||||
client.subscribe("wattpilot/properties/lmo/state");
|
||||
client.subscribe("wattpilot/properties/ftt/state");
|
||||
client.subscribe("wattpilot/properties/fte/state");
|
||||
client.subscribe("wattpilot/properties/amp/state");
|
||||
client.subscribe("wattpilot/properties/car/state");
|
||||
client.subscribe("go-eCharger/270003/amp");
|
||||
client.subscribe("go-eCharger/270003/ate");
|
||||
client.subscribe("go-eCharger/270003/lmo");
|
||||
client.subscribe("go-eCharger/270003/att");
|
||||
client.subscribe("go-eCharger/270003/car");
|
||||
client.subscribe("weatherStation/#");
|
||||
});
|
||||
client.on("error", function (error) {
|
||||
//alert("MQTT Error: " + error);
|
||||
});
|
||||
client.on('end', function () {
|
||||
setTimeout(getMQTT, 5000);
|
||||
alert("MQTT Disconnected, try to reconnect in 5 secs.");
|
||||
})
|
||||
|
||||
function getNestedProp(obj, path) {
|
||||
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
|
||||
}
|
||||
function setNestedProp(obj, path, value) {
|
||||
var schema = obj; // a moving reference to internal objects within obj
|
||||
var pList = path.split('/');
|
||||
var len = pList.length;
|
||||
for (var i = 0; i < len - 1; i++) {
|
||||
var elem = pList[i];
|
||||
if (!schema[elem]) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[pList[len - 1]] = value;
|
||||
}
|
||||
|
||||
function messageReceived(topic, message) {
|
||||
setNestedProp(mqttData, topic, message);
|
||||
if (topic == "solarManager/P_Load") {
|
||||
setTimeout(function () { solarSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
|
||||
} else if (topic == "weatherStation/windDeg") {
|
||||
setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation"]?.[name]) }, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const solarSVG = {
|
||||
updateCnt: 99,
|
||||
updateValuesMQTT: function (mqttData) {
|
||||
if (this.updateCnt > 10) {
|
||||
this.updateCnt = 0;
|
||||
var obj = document.querySelector("object");
|
||||
var htmlNode = obj.contentDocument;
|
||||
htmlNode.getElementById("PufferOtxt").innerHTML = mqttData["solarManager"]["t_buffT"] + " °C";
|
||||
htmlNode.getElementById("PufferMtxt").innerHTML = mqttData["solarManager"]["t_buffM"] + " °C";
|
||||
htmlNode.getElementById("PufferUtxt").innerHTML = mqttData["solarManager"]["t_buffB"] + " °C";
|
||||
htmlNode.getElementById("heaterVL").innerHTML = mqttData["solarManager"]["t_heatVL"] + " °C";
|
||||
htmlNode.getElementById("heaterRL").innerHTML = mqttData["solarManager"]["t_heatRL"] + " °C";
|
||||
htmlNode.getElementById("thermeVLfb").innerHTML = mqttData["solarManager"]["t_gasVLu"] + " °C";
|
||||
htmlNode.getElementById("thermeVLww").innerHTML = mqttData["solarManager"]["t_gasVLo"] + " °C";
|
||||
htmlNode.getElementById("thermeRL").innerHTML = mqttData["solarManager"]["t_gasRL"] + " °C";
|
||||
htmlNode.getElementById("fbVL").innerHTML = mqttData["solarManager"]["t_fbVL"] + " °C";
|
||||
htmlNode.getElementById("fbRL").innerHTML = mqttData["solarManager"]["t_fbRL"] + " °C";
|
||||
htmlNode.getElementById("triac").innerHTML = mqttData["solarManager"]["t_triac"] + " °C";
|
||||
}
|
||||
this.updateCnt++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var chartSettings = {
|
||||
type: 'line',
|
||||
options: {
|
||||
animation: true,
|
||||
plugins: {
|
||||
annotation: {
|
||||
common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true },
|
||||
annotations: []
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
pointStyle: "circle",
|
||||
boxWidth: 4,
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.dataset.yAxisID == "y1") {
|
||||
label += Math.round(context.parsed.y * 10) / 10 + " " + "L/min";
|
||||
} else {
|
||||
if (context.parsed.y !== null) {
|
||||
ret = scale(Math.round(context.parsed.y), false);
|
||||
label += ret[0] + " " + ret[1] + "°C";
|
||||
}
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
pointStyleWidth: 10,
|
||||
usePointStyle: true,
|
||||
pointStyle: "line",
|
||||
}
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
adapters: {
|
||||
date: {
|
||||
locale: "DE-de"
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
|
||||
},
|
||||
type: 'timestack',
|
||||
},
|
||||
y: {
|
||||
stacked: false,
|
||||
display: true,
|
||||
position: 'left',
|
||||
ticks: {
|
||||
callback: value => `${value} °C`,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Temperatur"
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
stacked: false,
|
||||
display: true,
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: value => `${value} L/min`,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Wasserverbrauch"
|
||||
},
|
||||
data:{}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var chartData = {};
|
||||
const heatChart = new Chart(
|
||||
document.querySelector('#heat-chart'),
|
||||
Object.assign({}, chartSettings)
|
||||
);
|
||||
const waterChart = new Chart(
|
||||
document.querySelector('#water-chart'),
|
||||
Object.assign({}, chartSettings)
|
||||
);
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
solarMQTT.getMQTT();
|
||||
getData(heatChart, 'ajax/getHeaterData.php', { sunrise: { FROM: -24, TO: 0 } });
|
||||
getData(waterChart, 'ajax/getWaterData.php', { sunrise: { FROM: -24, TO: 0 } });
|
||||
}
|
||||
});
|
||||
|
||||
+152
-152
@@ -1,152 +1,152 @@
|
||||
tooltipLabel = function (context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.dataset.yAxisID == "y1") {
|
||||
label += Math.round(context.parsed.y * 10) / 10 + " " + "%";
|
||||
} else {
|
||||
if (context.parsed.y !== null) {
|
||||
ret = scale(Math.round(context.parsed.y), false);
|
||||
label += ret[0] + " " + ret[1] + "Wh";
|
||||
}
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
tooltipFooter = function (tooltipItems){
|
||||
let sum = 0;
|
||||
|
||||
tooltipItems.forEach(function(tooltipItem) {
|
||||
if (tooltipItem.dataset.yAxisID != "y1") {
|
||||
sum += tooltipItem.parsed.y;
|
||||
}
|
||||
});
|
||||
ret = scale(Math.round(sum), false);
|
||||
sum = ret[0] + " " + ret[1] + "Wh";
|
||||
return 'Summe: ' + sum;
|
||||
}
|
||||
|
||||
legendLabels = function(chart){
|
||||
const datasets = chart.data.datasets;
|
||||
const {
|
||||
labels: {
|
||||
usePointStyle,
|
||||
pointStyle,
|
||||
textAlign,
|
||||
color
|
||||
}
|
||||
} = chart.legend.options;
|
||||
return chart._getSortedDatasetMetas().map((meta) => {
|
||||
const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);
|
||||
const borderWidth = Chart.helpers.toPadding(style.borderWidth);
|
||||
ret = scale(Math.round(arraySum(datasets[meta.index].data)), false);
|
||||
ret2 = scale(Math.round(datasets[meta.index].data[datasets[meta.index].data.length-1]), false);
|
||||
return {
|
||||
text: datasets[meta.index].label + " Σ " + ret[0]+" "+ret[1]+"Wh",//+ " Last: " + ret2[0]+" "+ret2[1]+"W",
|
||||
fillStyle: style.backgroundColor,
|
||||
fontColor: color,
|
||||
hidden: !meta.visible,
|
||||
lineCap: style.borderCapStyle,
|
||||
lineDash: style.borderDash,
|
||||
lineDashOffset: style.borderDashOffset,
|
||||
lineJoin: style.borderJoinStyle,
|
||||
lineWidth: (borderWidth.width + borderWidth.height) / 4,
|
||||
strokeStyle: style.borderColor,
|
||||
pointStyle: pointStyle || style.pointStyle,
|
||||
rotation: style.rotation,
|
||||
textAlign: textAlign || style.textAlign,
|
||||
borderRadius: 0, // TODO: v4, default to style.borderRadius
|
||||
datasetIndex: meta.index
|
||||
};
|
||||
}, this);
|
||||
}
|
||||
Chart.defaults.plugins.tooltip.callbacks.footer = tooltipFooter;
|
||||
Chart.defaults.plugins.tooltip.callbacks.label = tooltipLabel;
|
||||
Chart.defaults.plugins.legend.labels.generateLabels = legendLabels;
|
||||
|
||||
var forecastChartSettings = {
|
||||
type: 'bar',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
display: true,
|
||||
min: 0,
|
||||
suggestedMax: 1000,
|
||||
ticks: {
|
||||
callback: value => `${value / 1000} kWh`,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var decadeChartSettings = {
|
||||
type: 'bar',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
display: true,
|
||||
min: 0,
|
||||
suggestedMax: 1000000,
|
||||
ticks: {
|
||||
callback: value => `${value / 1000000} MWh`,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var chartData = {};
|
||||
|
||||
const consChart = new Chart(
|
||||
document.querySelector('#consumption-chart'),
|
||||
Object.assign({}, forecastChartSettings)
|
||||
);
|
||||
const prodChart = new Chart(
|
||||
document.querySelector('#production-chart'),
|
||||
Object.assign({}, forecastChartSettings)
|
||||
);
|
||||
const consChartYear = new Chart(
|
||||
document.querySelector('#consumption-chart-year'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const prodChartYear = new Chart(
|
||||
document.querySelector('#production-chart-year'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const consChartDecade = new Chart(
|
||||
document.querySelector('#consumption-chart-decade'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const prodChartDecade = new Chart(
|
||||
document.querySelector('#production-chart-decade'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
getData(prodChart, 'ajax/energyHistory.php?series=prod&range=month');
|
||||
getData(consChart, 'ajax/energyHistory.php?series=cons&range=month');
|
||||
getData(prodChartYear, 'ajax/energyHistory.php?series=prod&range=year');
|
||||
getData(consChartYear, 'ajax/energyHistory.php?series=cons&range=year');
|
||||
getData(prodChartDecade, 'ajax/energyHistory.php?series=prod&range=decade');
|
||||
getData(consChartDecade, 'ajax/energyHistory.php?series=cons&range=decade');
|
||||
getStats("Stats-Year","ajax/getStats.php?type=ThisYear");
|
||||
getStats("Stats-Lastyear","ajax/getStats.php?type=LastYear");
|
||||
getStats("Stats-Prelastyear","ajax/getStats.php?type=PreLastYear");
|
||||
}
|
||||
});
|
||||
tooltipLabel = function (context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.dataset.yAxisID == "y1") {
|
||||
label += Math.round(context.parsed.y * 10) / 10 + " " + "%";
|
||||
} else {
|
||||
if (context.parsed.y !== null) {
|
||||
ret = scale(Math.round(context.parsed.y), false);
|
||||
label += ret[0] + " " + ret[1] + "Wh";
|
||||
}
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
tooltipFooter = function (tooltipItems){
|
||||
let sum = 0;
|
||||
|
||||
tooltipItems.forEach(function(tooltipItem) {
|
||||
if (tooltipItem.dataset.yAxisID != "y1") {
|
||||
sum += tooltipItem.parsed.y;
|
||||
}
|
||||
});
|
||||
ret = scale(Math.round(sum), false);
|
||||
sum = ret[0] + " " + ret[1] + "Wh";
|
||||
return 'Summe: ' + sum;
|
||||
}
|
||||
|
||||
legendLabels = function(chart){
|
||||
const datasets = chart.data.datasets;
|
||||
const {
|
||||
labels: {
|
||||
usePointStyle,
|
||||
pointStyle,
|
||||
textAlign,
|
||||
color
|
||||
}
|
||||
} = chart.legend.options;
|
||||
return chart._getSortedDatasetMetas().map((meta) => {
|
||||
const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);
|
||||
const borderWidth = Chart.helpers.toPadding(style.borderWidth);
|
||||
ret = scale(Math.round(arraySum(datasets[meta.index].data)), false);
|
||||
ret2 = scale(Math.round(datasets[meta.index].data[datasets[meta.index].data.length-1]), false);
|
||||
return {
|
||||
text: datasets[meta.index].label + " Σ " + ret[0]+" "+ret[1]+"Wh",//+ " Last: " + ret2[0]+" "+ret2[1]+"W",
|
||||
fillStyle: style.backgroundColor,
|
||||
fontColor: color,
|
||||
hidden: !meta.visible,
|
||||
lineCap: style.borderCapStyle,
|
||||
lineDash: style.borderDash,
|
||||
lineDashOffset: style.borderDashOffset,
|
||||
lineJoin: style.borderJoinStyle,
|
||||
lineWidth: (borderWidth.width + borderWidth.height) / 4,
|
||||
strokeStyle: style.borderColor,
|
||||
pointStyle: pointStyle || style.pointStyle,
|
||||
rotation: style.rotation,
|
||||
textAlign: textAlign || style.textAlign,
|
||||
borderRadius: 0, // TODO: v4, default to style.borderRadius
|
||||
datasetIndex: meta.index
|
||||
};
|
||||
}, this);
|
||||
}
|
||||
Chart.defaults.plugins.tooltip.callbacks.footer = tooltipFooter;
|
||||
Chart.defaults.plugins.tooltip.callbacks.label = tooltipLabel;
|
||||
Chart.defaults.plugins.legend.labels.generateLabels = legendLabels;
|
||||
|
||||
var forecastChartSettings = {
|
||||
type: 'bar',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
display: true,
|
||||
min: 0,
|
||||
suggestedMax: 1000,
|
||||
ticks: {
|
||||
callback: value => `${value / 1000} kWh`,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var decadeChartSettings = {
|
||||
type: 'bar',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
display: true,
|
||||
min: 0,
|
||||
suggestedMax: 1000000,
|
||||
ticks: {
|
||||
callback: value => `${value / 1000000} MWh`,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var chartData = {};
|
||||
|
||||
const consChart = new Chart(
|
||||
document.querySelector('#consumption-chart'),
|
||||
Object.assign({}, forecastChartSettings)
|
||||
);
|
||||
const prodChart = new Chart(
|
||||
document.querySelector('#production-chart'),
|
||||
Object.assign({}, forecastChartSettings)
|
||||
);
|
||||
const consChartYear = new Chart(
|
||||
document.querySelector('#consumption-chart-year'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const prodChartYear = new Chart(
|
||||
document.querySelector('#production-chart-year'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const consChartDecade = new Chart(
|
||||
document.querySelector('#consumption-chart-decade'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
const prodChartDecade = new Chart(
|
||||
document.querySelector('#production-chart-decade'),
|
||||
Object.assign({}, decadeChartSettings)
|
||||
);
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
getData(prodChart, 'ajax/energyHistory.php?series=prod&range=month');
|
||||
getData(consChart, 'ajax/energyHistory.php?series=cons&range=month');
|
||||
getData(prodChartYear, 'ajax/energyHistory.php?series=prod&range=year');
|
||||
getData(consChartYear, 'ajax/energyHistory.php?series=cons&range=year');
|
||||
getData(prodChartDecade, 'ajax/energyHistory.php?series=prod&range=decade');
|
||||
getData(consChartDecade, 'ajax/energyHistory.php?series=cons&range=decade');
|
||||
getStats("Stats-Year","ajax/getStats.php?type=ThisYear");
|
||||
getStats("Stats-Lastyear","ajax/getStats.php?type=LastYear");
|
||||
getStats("Stats-Prelastyear","ajax/getStats.php?type=PreLastYear");
|
||||
}
|
||||
});
|
||||
|
||||
+422
-422
@@ -1,59 +1,59 @@
|
||||
|
||||
const homeMQTT = {
|
||||
getMQTT: function () {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const topic = "#";
|
||||
const connection = "wss://mqtt.nas.el-wa.org:443"
|
||||
mqttsolarTreeDone = false;
|
||||
// const connection = "ws://username:password@37.97.203.138:8083" // Works
|
||||
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
|
||||
const client = mqtt.connect(connection, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
client.on("message", messageReceived);
|
||||
client.on("connect", function () {
|
||||
// Welche Zweige gebraucht werden, sagt die Raumtabelle: seit eine
|
||||
// Kachel jeden Messwert zeigen kann, reicht Raumtemp allein nicht
|
||||
// mehr. homeTopics kommt aus restricted/rooms.php.
|
||||
homeTopics.forEach(function (topic) { client.subscribe(topic); });
|
||||
});
|
||||
client.on("error", function (error) {
|
||||
//alert("MQTT Error: " + error);
|
||||
});
|
||||
client.on('end', function () {
|
||||
setTimeout(getMQTT, 5000);
|
||||
alert("MQTT Disconnected, try to reconnect in 5 secs.");
|
||||
})
|
||||
|
||||
function getNestedProp(obj, path) {
|
||||
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
|
||||
}
|
||||
function setNestedProp(obj, path, value) {
|
||||
var schema = obj; // a moving reference to internal objects within obj
|
||||
var pList = path.split('/');
|
||||
var len = pList.length;
|
||||
for (var i = 0; i < len - 1; i++) {
|
||||
var elem = pList[i];
|
||||
if (!schema[elem]) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[pList[len - 1]] = value;
|
||||
}
|
||||
|
||||
function messageReceived(topic, message) {
|
||||
setNestedProp(mqttData, topic, message);
|
||||
setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const homeSVG = {
|
||||
updateCnt: 99,
|
||||
fillElementArray: function () {
|
||||
|
||||
},
|
||||
|
||||
const homeMQTT = {
|
||||
getMQTT: function () {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const topic = "#";
|
||||
const connection = "wss://mqtt.nas.el-wa.org:443"
|
||||
mqttsolarTreeDone = false;
|
||||
// const connection = "ws://username:password@37.97.203.138:8083" // Works
|
||||
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
|
||||
const client = mqtt.connect(connection, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
client.on("message", messageReceived);
|
||||
client.on("connect", function () {
|
||||
// Welche Zweige gebraucht werden, sagt die Raumtabelle: seit eine
|
||||
// Kachel jeden Messwert zeigen kann, reicht Raumtemp allein nicht
|
||||
// mehr. homeTopics kommt aus restricted/rooms.php.
|
||||
homeTopics.forEach(function (topic) { client.subscribe(topic); });
|
||||
});
|
||||
client.on("error", function (error) {
|
||||
//alert("MQTT Error: " + error);
|
||||
});
|
||||
client.on('end', function () {
|
||||
setTimeout(getMQTT, 5000);
|
||||
alert("MQTT Disconnected, try to reconnect in 5 secs.");
|
||||
})
|
||||
|
||||
function getNestedProp(obj, path) {
|
||||
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
|
||||
}
|
||||
function setNestedProp(obj, path, value) {
|
||||
var schema = obj; // a moving reference to internal objects within obj
|
||||
var pList = path.split('/');
|
||||
var len = pList.length;
|
||||
for (var i = 0; i < len - 1; i++) {
|
||||
var elem = pList[i];
|
||||
if (!schema[elem]) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[pList[len - 1]] = value;
|
||||
}
|
||||
|
||||
function messageReceived(topic, message) {
|
||||
setNestedProp(mqttData, topic, message);
|
||||
setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const homeSVG = {
|
||||
updateCnt: 99,
|
||||
fillElementArray: function () {
|
||||
|
||||
},
|
||||
/**
|
||||
* Eine Nachricht aus dem Baum holen. mqttData ist nach den Teilen des
|
||||
* Topics verschachtelt, siehe setNestedProp() weiter oben.
|
||||
@@ -116,369 +116,369 @@ const homeSVG = {
|
||||
if (typeof (values["mode"]) != "undefined")
|
||||
el(room.id + "_buffer").setAttribute("display", values["mode"] == "Overheating" ? "" : "none");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var currentFloor = "OG";
|
||||
|
||||
function addClass(el, classNameToAdd){
|
||||
el.className += ' ' + classNameToAdd;
|
||||
}
|
||||
|
||||
function removeClass(el, classNameToRemove){
|
||||
var elClass = ' ' + el.className + ' ';
|
||||
while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){
|
||||
elClass = elClass.replace(' ' + classNameToRemove + ' ', '');
|
||||
}
|
||||
el.className = elClass;
|
||||
}
|
||||
|
||||
// Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die
|
||||
// home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier
|
||||
// sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht.
|
||||
function switchTab(newtab){
|
||||
const newContent = document.getElementById(newtab);
|
||||
const newTabBtn = document.getElementById(newtab+"-tab");
|
||||
document.querySelectorAll("#actions-tab .nav-link").forEach(function(btn){
|
||||
removeClass(btn,"active");
|
||||
});
|
||||
document.querySelectorAll("#actions-tabContent .tab-pane").forEach(function(pane){
|
||||
removeClass(pane,"active show");
|
||||
});
|
||||
addClass(newTabBtn,"active");
|
||||
addClass(newContent,"active show");
|
||||
}
|
||||
|
||||
|
||||
function switchFloor(floor){
|
||||
if(currentFloor == floor)
|
||||
return;
|
||||
const targetIn = document.getElementById(floor+'_Info');
|
||||
const targetOut = document.getElementById(currentFloor+'_Info');
|
||||
var blendIn = new KeyframeEffect(
|
||||
targetIn, [{opacity: '0'},{opacity: '100'}],
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in-out",
|
||||
fill: "forwards",
|
||||
iterations: 1,
|
||||
}
|
||||
);
|
||||
var blendOut = new KeyframeEffect(
|
||||
targetOut, [{opacity: '100'},{opacity: '0'}],
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in-out",
|
||||
fill: "forwards",
|
||||
iterations: 1,
|
||||
}
|
||||
);
|
||||
var inAnim = new Animation(blendIn,document.timeline);
|
||||
var outAnim = new Animation(blendOut,document.timeline);
|
||||
targetIn.setAttribute("display","");
|
||||
outAnim.onfinish= (event) => {
|
||||
targetOut.setAttribute("display","none");
|
||||
};
|
||||
inAnim.play();
|
||||
outAnim.play();
|
||||
currentFloor = floor;
|
||||
}
|
||||
|
||||
|
||||
var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), {
|
||||
keyboard: false
|
||||
});
|
||||
var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), {
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
var mqttData = {};
|
||||
|
||||
function openHeaterSettings(heater){
|
||||
openRoomView(heater);
|
||||
}
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
homeMQTT.getMQTT();
|
||||
mountMeteogram();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen,
|
||||
* Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum
|
||||
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
|
||||
* Zimmer waren von hier gar nicht erreichbar.
|
||||
*
|
||||
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room).
|
||||
*/
|
||||
function openRoomView(raum) {
|
||||
const teile = raum.toString().split("_");
|
||||
document.getElementById("modal-title").innerHTML =
|
||||
teile[0] + " · " + teile.slice(1).join(" ")
|
||||
.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö");
|
||||
|
||||
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
||||
// Zuhoerer stellt sicher, dass nicht der Handler eines vorher
|
||||
// geoeffneten Modals mitfeuert; im Raum-Modal bleibt er weg. Hier wird
|
||||
// nichts gesammelt und dann abgeschickt - jeder Rollladen, jeder
|
||||
// Schalter und jede Lampe wirkt sofort, und die Solltemperatur tut es
|
||||
// jetzt auch. Ein Knopf, der nur fuer einen der Reiter gilt, waere in
|
||||
// allen anderen eine Falle.
|
||||
const alt = document.getElementById("modalSaveBtn");
|
||||
const btn = alt.cloneNode(true);
|
||||
alt.replaceWith(btn);
|
||||
btn.hidden = true;
|
||||
|
||||
const koerper = document.getElementById("modal-body");
|
||||
koerper.innerHTML = loadingHTML("Wird geladen...");
|
||||
modalEV.show();
|
||||
|
||||
fetch("./ajax/room.php?room=" + encodeURIComponent(raum), {
|
||||
method: "GET",
|
||||
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
|
||||
})
|
||||
.then(a => a.text())
|
||||
.then(html => {
|
||||
koerper.innerHTML = html;
|
||||
|
||||
const slider = document.getElementById("modal-slider");
|
||||
if (slider) {
|
||||
bindModalSlider(slider, document.getElementById("modal-slider-label"),
|
||||
wert => wert + " °C");
|
||||
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
|
||||
slider.value = typeof stand !== "undefined" ? stand : 10;
|
||||
// Nur "input": das zeichnet die Anzeige, schickt aber nichts.
|
||||
slider.dispatchEvent(new Event("input"));
|
||||
// "change" kommt beim Loslassen - wie bei den Rollladenreglern.
|
||||
slider.addEventListener("change", () => sendeSolltemperatur(raum));
|
||||
}
|
||||
|
||||
raumBedienungAktivieren(koerper);
|
||||
raumMesswerteAktualisieren(teile[0], teile[1]);
|
||||
})
|
||||
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden.
|
||||
*
|
||||
* Welches Geraet welches Element bekommt, entscheidet der Server
|
||||
* (restricted/roomControls.php); hier haengen nur die Zuhoerer an den
|
||||
* data-Attributen, die er dabei setzt.
|
||||
*/
|
||||
function raumBedienungAktivieren(koerper) {
|
||||
koerper.querySelectorAll("button[data-cmd]").forEach(k => {
|
||||
k.addEventListener("click", () => schalte(k));
|
||||
});
|
||||
|
||||
// Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird
|
||||
// erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein
|
||||
// neues Ziel und kaeme aus dem Anfahren nicht mehr heraus.
|
||||
koerper.querySelectorAll(".raum-jalousie").forEach(lamellenZeichnen);
|
||||
koerper.querySelectorAll(".raum-regler").forEach(regler => {
|
||||
const schieber = regler.querySelector("input[type=range]");
|
||||
reglerAnzeigen(regler);
|
||||
schieber.addEventListener("input", () => reglerAnzeigen(regler));
|
||||
schieber.addEventListener("change", () => {
|
||||
const params = {};
|
||||
params[regler.dataset.param] = schieber.value;
|
||||
raumKommando(Number(regler.dataset.cmd), params, regler);
|
||||
});
|
||||
});
|
||||
|
||||
// Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das
|
||||
// Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung
|
||||
// hinterher, weil wir sie selbst gesetzt haben.
|
||||
koerper.querySelectorAll(".raum-schalter").forEach(schalter => {
|
||||
const kasten = schalter.querySelector("input[type=checkbox]");
|
||||
const beschriftung = schalter.querySelector("label");
|
||||
kasten.addEventListener("change", () => {
|
||||
const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus;
|
||||
const cmd = ziel || schalter.dataset.um;
|
||||
beschriftung.textContent = kasten.checked ? "An" : "Aus";
|
||||
if (!cmd) return;
|
||||
raumKommando(Number(cmd), {}, schalter);
|
||||
});
|
||||
});
|
||||
|
||||
// Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei
|
||||
// Zahlen 0-255. Zerlegt wird deshalb erst hier.
|
||||
koerper.querySelectorAll(".raum-farbe").forEach(feld => {
|
||||
const eingabe = feld.querySelector("input[type=color]");
|
||||
eingabe.addEventListener("change", () => {
|
||||
const hex = eingabe.value;
|
||||
const params = {};
|
||||
params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16);
|
||||
params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16);
|
||||
params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16);
|
||||
raumKommando(Number(feld.dataset.cmd), params, feld);
|
||||
});
|
||||
});
|
||||
|
||||
// Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset.
|
||||
koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => {
|
||||
auswahl.addEventListener("change", () => {
|
||||
const params = {};
|
||||
params[auswahl.dataset.param] = auswahl.value;
|
||||
raumKommando(Number(auswahl.dataset.cmd), params, auswahl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen.
|
||||
*
|
||||
* Was 0 % und 100 % bedeuten, steht bei den senkrechten Reglern als Wort
|
||||
* an ihren Enden ("oben"/"unten", "auf"/"zu"); eine Zahl steht dort nicht
|
||||
* mehr - das Bild daneben zeigt beim Ziehen mehr als sie.
|
||||
*/
|
||||
function reglerAnzeigen(regler) {
|
||||
const schieber = regler.querySelector("input[type=range]");
|
||||
const anzeige = regler.querySelector(".raum-regler-wert");
|
||||
const wert = Number(schieber.value);
|
||||
|
||||
if (anzeige) {
|
||||
anzeige.textContent = wert + (schieber.dataset.einheit || "");
|
||||
}
|
||||
|
||||
// Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch
|
||||
// bindModalSlider() in common.js den Temperaturregler faerbt.
|
||||
const min = Number(schieber.min);
|
||||
const max = Number(schieber.max);
|
||||
schieber.style.setProperty("--background-size",
|
||||
((wert - min) * 100 / (max - min)) + "%");
|
||||
|
||||
lamellenZeichnen(regler.closest(".raum-jalousie"));
|
||||
}
|
||||
|
||||
/** Der Stand eines der beiden Regler einer Jalousie, als Anteil 0..1. */
|
||||
function reglerAnteil(jalousie, rolle, ersatz) {
|
||||
const schieber = jalousie.querySelector("[data-rolle=" + rolle + "] input[type=range]");
|
||||
if (!schieber) return ersatz;
|
||||
const min = Number(schieber.min);
|
||||
return (Number(schieber.value) - min) / (Number(schieber.max) - min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Fenster zeichnen, wie die Jalousie davor gerade steht.
|
||||
*
|
||||
* Zwei Zahlen in einem Bild. Die Hoehe schneidet ab - gezeichnet wird nur
|
||||
* der Teil des Fensters, den die Jalousie bedeckt, mit der Endschiene an
|
||||
* seiner Unterkante. Die Neigung bestimmt, wie dick eine Lamelle von vorn
|
||||
* erscheint: w * sin(Winkel). Bei "auf" sieht man nur ihre Kante und viel
|
||||
* Luft dazwischen, bei "zu" deckt sie den Abstand zur naechsten
|
||||
* vollstaendig ab - genau das, was das Licht abhaelt. Von Mitte zu Mitte
|
||||
* sind es 10 Einheiten, die Lamelle ist 11 tief; ab etwa 65 % schliesst
|
||||
* das Bild, so wie es die Jalousie auch tut.
|
||||
*
|
||||
* Ein Rollladen ohne Neigung bekommt volle Lamellenhoehe - er ist ja ein
|
||||
* geschlossener Panzer.
|
||||
*/
|
||||
function lamellenZeichnen(jalousie) {
|
||||
if (!jalousie) return;
|
||||
const bild = jalousie.querySelector(".raum-lamellen");
|
||||
if (!bild) return;
|
||||
|
||||
const hoch = bild.viewBox.baseVal.height;
|
||||
const abdeckung = hoch * reglerAnteil(jalousie, "position", 1);
|
||||
const kippung = reglerAnteil(jalousie, "neigung", 1);
|
||||
const dicke = Math.max(1.6, 11 * Math.sin(kippung * Math.PI / 2));
|
||||
|
||||
bild.querySelector(".raum-abdeckung").setAttribute("height", abdeckung);
|
||||
bild.querySelectorAll(".raum-lamelle").forEach(lamelle => {
|
||||
const mitte = Number(lamelle.dataset.mitte);
|
||||
lamelle.setAttribute("height", dicke);
|
||||
lamelle.setAttribute("y", mitte - dicke / 2);
|
||||
});
|
||||
// Die Endschiene sitzt auf der Unterkante und bleibt im Bild, auch wenn
|
||||
// die Jalousie ganz oben steht.
|
||||
const schiene = bild.querySelector(".raum-endschiene");
|
||||
const dick = schiene.height.baseVal.value;
|
||||
schiene.setAttribute("y", Math.max(0, Math.min(hoch - dick, abdeckung - dick)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen.
|
||||
*
|
||||
* Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der
|
||||
* Datenbank waere neben dem Regler nur ein Stand von vorhin.
|
||||
*/
|
||||
function raumMesswerteAktualisieren(etage, raum) {
|
||||
const werte = mqttData["Raumtemp"]?.[etage]?.[raum];
|
||||
if (!werte) return;
|
||||
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
|
||||
const wert = werte[feld.dataset.mqtt];
|
||||
if (typeof wert !== "undefined") feld.textContent = wert;
|
||||
});
|
||||
const heizt = document.querySelector("#roomView [data-mqtt-heizung]");
|
||||
if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true");
|
||||
}
|
||||
|
||||
/** Ein Kommando des angeklickten Knopfes ausfuehren. */
|
||||
function schalte(knopf) {
|
||||
const zeile = knopf.closest("[data-cmdrow]");
|
||||
const params = {};
|
||||
if (zeile) {
|
||||
zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; });
|
||||
}
|
||||
raumKommando(Number(knopf.dataset.cmd), params, knopf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Kommando schicken und die Karte so lange sperren.
|
||||
*
|
||||
* Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will
|
||||
* zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare
|
||||
* Sperre tippt man in der Zeit dreimal nach.
|
||||
*/
|
||||
function raumKommando(cmdId, params, element) {
|
||||
const karte = element.closest(".raum-geraet") || element;
|
||||
karte.classList.add("raum-warte");
|
||||
raumRequest("command", { command_id: cmdId, params: params })
|
||||
.then(antwort => {
|
||||
karte.classList.remove("raum-warte");
|
||||
raumMeldung(antwort);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Die neue Solltemperatur schicken.
|
||||
*
|
||||
* Das Modal bleibt danach offen: frueher schloss es sich, weil der
|
||||
* Speichern-Knopf das Ende der Bedienung war. Jetzt ist das Verschieben
|
||||
* des Reglers die Bedienung, und wer dreimal nachjustiert, will nicht
|
||||
* dreimal neu aufmachen.
|
||||
*/
|
||||
function sendeSolltemperatur(raum) {
|
||||
const slider = document.getElementById("modal-slider");
|
||||
if (!slider) return;
|
||||
const karte = slider.closest(".raum-geraet");
|
||||
if (karte) karte.classList.add("raum-warte");
|
||||
raumRequest("temp", { room: raum, temp: Number(slider.value) })
|
||||
.then(antwort => {
|
||||
if (karte) karte.classList.remove("raum-warte");
|
||||
raumMeldung(antwort);
|
||||
});
|
||||
}
|
||||
|
||||
function raumRequest(action, body) {
|
||||
return fetch("./ajax/room.php?action=" + action, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(a => a.json())
|
||||
.catch(fehler => ({ error: fehler.message }));
|
||||
}
|
||||
|
||||
/** Rueckmeldung im Modal statt als alert - man schaltet oft mehrmals. */
|
||||
function raumMeldung(antwort) {
|
||||
const ziel = document.getElementById("roomFeedback");
|
||||
if (!ziel) return;
|
||||
ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary");
|
||||
ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var currentFloor = "OG";
|
||||
|
||||
function addClass(el, classNameToAdd){
|
||||
el.className += ' ' + classNameToAdd;
|
||||
}
|
||||
|
||||
function removeClass(el, classNameToRemove){
|
||||
var elClass = ' ' + el.className + ' ';
|
||||
while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){
|
||||
elClass = elClass.replace(' ' + classNameToRemove + ' ', '');
|
||||
}
|
||||
el.className = elClass;
|
||||
}
|
||||
|
||||
// Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die
|
||||
// home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier
|
||||
// sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht.
|
||||
function switchTab(newtab){
|
||||
const newContent = document.getElementById(newtab);
|
||||
const newTabBtn = document.getElementById(newtab+"-tab");
|
||||
document.querySelectorAll("#actions-tab .nav-link").forEach(function(btn){
|
||||
removeClass(btn,"active");
|
||||
});
|
||||
document.querySelectorAll("#actions-tabContent .tab-pane").forEach(function(pane){
|
||||
removeClass(pane,"active show");
|
||||
});
|
||||
addClass(newTabBtn,"active");
|
||||
addClass(newContent,"active show");
|
||||
}
|
||||
|
||||
|
||||
function switchFloor(floor){
|
||||
if(currentFloor == floor)
|
||||
return;
|
||||
const targetIn = document.getElementById(floor+'_Info');
|
||||
const targetOut = document.getElementById(currentFloor+'_Info');
|
||||
var blendIn = new KeyframeEffect(
|
||||
targetIn, [{opacity: '0'},{opacity: '100'}],
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in-out",
|
||||
fill: "forwards",
|
||||
iterations: 1,
|
||||
}
|
||||
);
|
||||
var blendOut = new KeyframeEffect(
|
||||
targetOut, [{opacity: '100'},{opacity: '0'}],
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in-out",
|
||||
fill: "forwards",
|
||||
iterations: 1,
|
||||
}
|
||||
);
|
||||
var inAnim = new Animation(blendIn,document.timeline);
|
||||
var outAnim = new Animation(blendOut,document.timeline);
|
||||
targetIn.setAttribute("display","");
|
||||
outAnim.onfinish= (event) => {
|
||||
targetOut.setAttribute("display","none");
|
||||
};
|
||||
inAnim.play();
|
||||
outAnim.play();
|
||||
currentFloor = floor;
|
||||
}
|
||||
|
||||
|
||||
var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), {
|
||||
keyboard: false
|
||||
});
|
||||
var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), {
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
var mqttData = {};
|
||||
|
||||
function openHeaterSettings(heater){
|
||||
openRoomView(heater);
|
||||
}
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (event.target.readyState === "complete") {
|
||||
homeMQTT.getMQTT();
|
||||
mountMeteogram();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen,
|
||||
* Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum
|
||||
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
|
||||
* Zimmer waren von hier gar nicht erreichbar.
|
||||
*
|
||||
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room).
|
||||
*/
|
||||
function openRoomView(raum) {
|
||||
const teile = raum.toString().split("_");
|
||||
document.getElementById("modal-title").innerHTML =
|
||||
teile[0] + " · " + teile.slice(1).join(" ")
|
||||
.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö");
|
||||
|
||||
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
||||
// Zuhoerer stellt sicher, dass nicht der Handler eines vorher
|
||||
// geoeffneten Modals mitfeuert; im Raum-Modal bleibt er weg. Hier wird
|
||||
// nichts gesammelt und dann abgeschickt - jeder Rollladen, jeder
|
||||
// Schalter und jede Lampe wirkt sofort, und die Solltemperatur tut es
|
||||
// jetzt auch. Ein Knopf, der nur fuer einen der Reiter gilt, waere in
|
||||
// allen anderen eine Falle.
|
||||
const alt = document.getElementById("modalSaveBtn");
|
||||
const btn = alt.cloneNode(true);
|
||||
alt.replaceWith(btn);
|
||||
btn.hidden = true;
|
||||
|
||||
const koerper = document.getElementById("modal-body");
|
||||
koerper.innerHTML = loadingHTML("Wird geladen...");
|
||||
modalEV.show();
|
||||
|
||||
fetch("./ajax/room.php?room=" + encodeURIComponent(raum), {
|
||||
method: "GET",
|
||||
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
|
||||
})
|
||||
.then(a => a.text())
|
||||
.then(html => {
|
||||
koerper.innerHTML = html;
|
||||
|
||||
const slider = document.getElementById("modal-slider");
|
||||
if (slider) {
|
||||
bindModalSlider(slider, document.getElementById("modal-slider-label"),
|
||||
wert => wert + " °C");
|
||||
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
|
||||
slider.value = typeof stand !== "undefined" ? stand : 10;
|
||||
// Nur "input": das zeichnet die Anzeige, schickt aber nichts.
|
||||
slider.dispatchEvent(new Event("input"));
|
||||
// "change" kommt beim Loslassen - wie bei den Rollladenreglern.
|
||||
slider.addEventListener("change", () => sendeSolltemperatur(raum));
|
||||
}
|
||||
|
||||
raumBedienungAktivieren(koerper);
|
||||
raumMesswerteAktualisieren(teile[0], teile[1]);
|
||||
})
|
||||
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden.
|
||||
*
|
||||
* Welches Geraet welches Element bekommt, entscheidet der Server
|
||||
* (restricted/roomControls.php); hier haengen nur die Zuhoerer an den
|
||||
* data-Attributen, die er dabei setzt.
|
||||
*/
|
||||
function raumBedienungAktivieren(koerper) {
|
||||
koerper.querySelectorAll("button[data-cmd]").forEach(k => {
|
||||
k.addEventListener("click", () => schalte(k));
|
||||
});
|
||||
|
||||
// Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird
|
||||
// erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein
|
||||
// neues Ziel und kaeme aus dem Anfahren nicht mehr heraus.
|
||||
koerper.querySelectorAll(".raum-jalousie").forEach(lamellenZeichnen);
|
||||
koerper.querySelectorAll(".raum-regler").forEach(regler => {
|
||||
const schieber = regler.querySelector("input[type=range]");
|
||||
reglerAnzeigen(regler);
|
||||
schieber.addEventListener("input", () => reglerAnzeigen(regler));
|
||||
schieber.addEventListener("change", () => {
|
||||
const params = {};
|
||||
params[regler.dataset.param] = schieber.value;
|
||||
raumKommando(Number(regler.dataset.cmd), params, regler);
|
||||
});
|
||||
});
|
||||
|
||||
// Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das
|
||||
// Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung
|
||||
// hinterher, weil wir sie selbst gesetzt haben.
|
||||
koerper.querySelectorAll(".raum-schalter").forEach(schalter => {
|
||||
const kasten = schalter.querySelector("input[type=checkbox]");
|
||||
const beschriftung = schalter.querySelector("label");
|
||||
kasten.addEventListener("change", () => {
|
||||
const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus;
|
||||
const cmd = ziel || schalter.dataset.um;
|
||||
beschriftung.textContent = kasten.checked ? "An" : "Aus";
|
||||
if (!cmd) return;
|
||||
raumKommando(Number(cmd), {}, schalter);
|
||||
});
|
||||
});
|
||||
|
||||
// Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei
|
||||
// Zahlen 0-255. Zerlegt wird deshalb erst hier.
|
||||
koerper.querySelectorAll(".raum-farbe").forEach(feld => {
|
||||
const eingabe = feld.querySelector("input[type=color]");
|
||||
eingabe.addEventListener("change", () => {
|
||||
const hex = eingabe.value;
|
||||
const params = {};
|
||||
params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16);
|
||||
params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16);
|
||||
params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16);
|
||||
raumKommando(Number(feld.dataset.cmd), params, feld);
|
||||
});
|
||||
});
|
||||
|
||||
// Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset.
|
||||
koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => {
|
||||
auswahl.addEventListener("change", () => {
|
||||
const params = {};
|
||||
params[auswahl.dataset.param] = auswahl.value;
|
||||
raumKommando(Number(auswahl.dataset.cmd), params, auswahl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen.
|
||||
*
|
||||
* Was 0 % und 100 % bedeuten, steht bei den senkrechten Reglern als Wort
|
||||
* an ihren Enden ("oben"/"unten", "auf"/"zu"); eine Zahl steht dort nicht
|
||||
* mehr - das Bild daneben zeigt beim Ziehen mehr als sie.
|
||||
*/
|
||||
function reglerAnzeigen(regler) {
|
||||
const schieber = regler.querySelector("input[type=range]");
|
||||
const anzeige = regler.querySelector(".raum-regler-wert");
|
||||
const wert = Number(schieber.value);
|
||||
|
||||
if (anzeige) {
|
||||
anzeige.textContent = wert + (schieber.dataset.einheit || "");
|
||||
}
|
||||
|
||||
// Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch
|
||||
// bindModalSlider() in common.js den Temperaturregler faerbt.
|
||||
const min = Number(schieber.min);
|
||||
const max = Number(schieber.max);
|
||||
schieber.style.setProperty("--background-size",
|
||||
((wert - min) * 100 / (max - min)) + "%");
|
||||
|
||||
lamellenZeichnen(regler.closest(".raum-jalousie"));
|
||||
}
|
||||
|
||||
/** Der Stand eines der beiden Regler einer Jalousie, als Anteil 0..1. */
|
||||
function reglerAnteil(jalousie, rolle, ersatz) {
|
||||
const schieber = jalousie.querySelector("[data-rolle=" + rolle + "] input[type=range]");
|
||||
if (!schieber) return ersatz;
|
||||
const min = Number(schieber.min);
|
||||
return (Number(schieber.value) - min) / (Number(schieber.max) - min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Fenster zeichnen, wie die Jalousie davor gerade steht.
|
||||
*
|
||||
* Zwei Zahlen in einem Bild. Die Hoehe schneidet ab - gezeichnet wird nur
|
||||
* der Teil des Fensters, den die Jalousie bedeckt, mit der Endschiene an
|
||||
* seiner Unterkante. Die Neigung bestimmt, wie dick eine Lamelle von vorn
|
||||
* erscheint: w * sin(Winkel). Bei "auf" sieht man nur ihre Kante und viel
|
||||
* Luft dazwischen, bei "zu" deckt sie den Abstand zur naechsten
|
||||
* vollstaendig ab - genau das, was das Licht abhaelt. Von Mitte zu Mitte
|
||||
* sind es 10 Einheiten, die Lamelle ist 11 tief; ab etwa 65 % schliesst
|
||||
* das Bild, so wie es die Jalousie auch tut.
|
||||
*
|
||||
* Ein Rollladen ohne Neigung bekommt volle Lamellenhoehe - er ist ja ein
|
||||
* geschlossener Panzer.
|
||||
*/
|
||||
function lamellenZeichnen(jalousie) {
|
||||
if (!jalousie) return;
|
||||
const bild = jalousie.querySelector(".raum-lamellen");
|
||||
if (!bild) return;
|
||||
|
||||
const hoch = bild.viewBox.baseVal.height;
|
||||
const abdeckung = hoch * reglerAnteil(jalousie, "position", 1);
|
||||
const kippung = reglerAnteil(jalousie, "neigung", 1);
|
||||
const dicke = Math.max(1.6, 11 * Math.sin(kippung * Math.PI / 2));
|
||||
|
||||
bild.querySelector(".raum-abdeckung").setAttribute("height", abdeckung);
|
||||
bild.querySelectorAll(".raum-lamelle").forEach(lamelle => {
|
||||
const mitte = Number(lamelle.dataset.mitte);
|
||||
lamelle.setAttribute("height", dicke);
|
||||
lamelle.setAttribute("y", mitte - dicke / 2);
|
||||
});
|
||||
// Die Endschiene sitzt auf der Unterkante und bleibt im Bild, auch wenn
|
||||
// die Jalousie ganz oben steht.
|
||||
const schiene = bild.querySelector(".raum-endschiene");
|
||||
const dick = schiene.height.baseVal.value;
|
||||
schiene.setAttribute("y", Math.max(0, Math.min(hoch - dick, abdeckung - dick)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen.
|
||||
*
|
||||
* Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der
|
||||
* Datenbank waere neben dem Regler nur ein Stand von vorhin.
|
||||
*/
|
||||
function raumMesswerteAktualisieren(etage, raum) {
|
||||
const werte = mqttData["Raumtemp"]?.[etage]?.[raum];
|
||||
if (!werte) return;
|
||||
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
|
||||
const wert = werte[feld.dataset.mqtt];
|
||||
if (typeof wert !== "undefined") feld.textContent = wert;
|
||||
});
|
||||
const heizt = document.querySelector("#roomView [data-mqtt-heizung]");
|
||||
if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true");
|
||||
}
|
||||
|
||||
/** Ein Kommando des angeklickten Knopfes ausfuehren. */
|
||||
function schalte(knopf) {
|
||||
const zeile = knopf.closest("[data-cmdrow]");
|
||||
const params = {};
|
||||
if (zeile) {
|
||||
zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; });
|
||||
}
|
||||
raumKommando(Number(knopf.dataset.cmd), params, knopf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Kommando schicken und die Karte so lange sperren.
|
||||
*
|
||||
* Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will
|
||||
* zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare
|
||||
* Sperre tippt man in der Zeit dreimal nach.
|
||||
*/
|
||||
function raumKommando(cmdId, params, element) {
|
||||
const karte = element.closest(".raum-geraet") || element;
|
||||
karte.classList.add("raum-warte");
|
||||
raumRequest("command", { command_id: cmdId, params: params })
|
||||
.then(antwort => {
|
||||
karte.classList.remove("raum-warte");
|
||||
raumMeldung(antwort);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Die neue Solltemperatur schicken.
|
||||
*
|
||||
* Das Modal bleibt danach offen: frueher schloss es sich, weil der
|
||||
* Speichern-Knopf das Ende der Bedienung war. Jetzt ist das Verschieben
|
||||
* des Reglers die Bedienung, und wer dreimal nachjustiert, will nicht
|
||||
* dreimal neu aufmachen.
|
||||
*/
|
||||
function sendeSolltemperatur(raum) {
|
||||
const slider = document.getElementById("modal-slider");
|
||||
if (!slider) return;
|
||||
const karte = slider.closest(".raum-geraet");
|
||||
if (karte) karte.classList.add("raum-warte");
|
||||
raumRequest("temp", { room: raum, temp: Number(slider.value) })
|
||||
.then(antwort => {
|
||||
if (karte) karte.classList.remove("raum-warte");
|
||||
raumMeldung(antwort);
|
||||
});
|
||||
}
|
||||
|
||||
function raumRequest(action, body) {
|
||||
return fetch("./ajax/room.php?action=" + action, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(a => a.json())
|
||||
.catch(fehler => ({ error: fehler.message }));
|
||||
}
|
||||
|
||||
/** Rueckmeldung im Modal statt als alert - man schaltet oft mehrmals. */
|
||||
function raumMeldung(antwort) {
|
||||
const ziel = document.getElementById("roomFeedback");
|
||||
if (!ziel) return;
|
||||
ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary");
|
||||
ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok;
|
||||
}
|
||||
|
||||
+1172
-1172
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user