From 1e495d380abf47f1b8341a67ca70dbc292466964 Mon Sep 17 00:00:00 2001 From: Thorsten Zoerner Date: Wed, 24 Apr 2024 12:48:54 +0200 Subject: [PATCH] increment: PowerFox/PowerOpti Support added to PWA and Backend --- framework/public_pwa/assets/js/index.js | 209 ++++----- framework/public_pwa/assets/js/powerfox.js | 469 +++++++++++++++++++++ framework/public_pwa/index.html | 2 +- framework/public_pwa/powerfox.html | 66 +++ framework/public_pwa/sitemap.xml | 3 + framework/services/api-pwa.service.js | 12 +- framework/services/httppull.service.js | 45 +- framework/services/powerfox.service.js | 219 ++++++++++ 8 files changed, 905 insertions(+), 120 deletions(-) create mode 100644 framework/public_pwa/assets/js/powerfox.js create mode 100644 framework/public_pwa/powerfox.html create mode 100644 framework/services/powerfox.service.js diff --git a/framework/public_pwa/assets/js/index.js b/framework/public_pwa/assets/js/index.js index 5d78719..d5e2029 100644 --- a/framework/public_pwa/assets/js/index.js +++ b/framework/public_pwa/assets/js/index.js @@ -27,43 +27,48 @@ const app = async function(token) { } const ctxChart = document.getElementById('forecastChart'); if(typeof window.chartObject !== 'undefined') window.chartObject.destroy(); - - window.chartObject = new Chart(ctxChart, { - type: 'bar', - data: { - labels: chartLabels, - datasets: [{ - label: 'Preis je kWh', - data: chartData, - backgroundColor:chartColors - }] - }, - options: { - scales: { - y: { - min: minY * 0.8 - } - }, - responsive: true, - - plugins: { - tooltip: { - callbacks: { - label: function(context) { - return context.parsed.y.toFixed(2).replace('.',',') + ' €/kWh'; + try { + window.chartObject = new Chart(ctxChart, { + type: 'bar', + data: { + labels: chartLabels, + datasets: [{ + label: 'Preis je kWh', + data: chartData, + backgroundColor:chartColors + }] + }, + options: { + scales: { + y: { + min: minY * 0.8 + } + }, + responsive: true, + + plugins: { + tooltip: { + callbacks: { + label: function(context) { + return context.parsed.y.toFixed(2).replace('.',',') + ' €/kWh'; + } + } + }, + legend: { + display:false + }, + datalabels: { + display: false, } } - }, - legend: { - display:false - }, - datalabels: { - display: false, } - } - } - }); + }); + + } catch(e) { + + } }); + let crossbalance = ''; if($.urlParam("crossbalance") || window.crossbalance) { @@ -243,76 +248,86 @@ const app = async function(token) { const xValues = consumptionChart.map(point => new Date( (point.x * 3600000)-demofy ).toLocaleString()); const yValues = consumptionChart.map(point => (point.y/1000)); const yValues2 = costChart.map(point => point.y); - - window.timelineChartObject = new Chart(ctxTimelineChart, { - type: 'line', - data: { - labels: xValues, // x-Werte als Labels - datasets: [{ - label: 'kWh', - yAxisID: 'A', - data: yValues, // y-Werte für die Datenpunkte - borderWidth: 1, - backgroundColor: '#273469', - borderColor: '#273469', - tension: 0.1 // Glättung der Linie - },{ - label: '€', - yAxisID: 'B', - data: yValues2, // y-Werte für die Datenpunkte - borderWidth: 1, - backgroundColor: '#606060', - borderColor: '#606060', - tension: 0.1 // Glättung der Linie - }] - }, - options: { - responsive: true, - scales: { - A: { - type: 'linear', - position: 'left', - ticks: { beginAtZero: true, color: '#273469' }, - // Hide grid lines, otherwise you have separate grid lines for the 2 y axes - grid: { display: false } - }, - B: { - type: 'linear', - position: 'right', - ticks: { beginAtZero: true, color: '#606060' }, - grid: { display: false } - }, - x: { ticks: { beginAtZero: true } } + + if(xValues.length > 1) { + window.timelineChartObject = new Chart(ctxTimelineChart, { + type: 'line', + data: { + labels: xValues, // x-Werte als Labels + datasets: [{ + label: 'kWh', + yAxisID: 'A', + data: yValues, // y-Werte für die Datenpunkte + borderWidth: 1, + backgroundColor: '#273469', + borderColor: '#273469', + tension: 0.1 // Glättung der Linie + },{ + label: '€', + yAxisID: 'B', + data: yValues2, // y-Werte für die Datenpunkte + borderWidth: 1, + backgroundColor: '#606060', + borderColor: '#606060', + tension: 0.1 // Glättung der Linie + }] }, - plugins: { - zoom: { - pan: { - mode: 'x', - enabled: true - }, + options: { + responsive: true, + scales: { + A: { + type: 'linear', + position: 'left', + ticks: { beginAtZero: true, color: '#273469' }, + // Hide grid lines, otherwise you have separate grid lines for the 2 y axes + grid: { display: false } + }, + B: { + type: 'linear', + position: 'right', + ticks: { beginAtZero: true, color: '#606060' }, + grid: { display: false } + }, + x: { ticks: { beginAtZero: true } } + }, + plugins: { zoom: { - wheel: { - enabled: true, - }, - pinch: { - enabled: true + pan: { + mode: 'x', + enabled: true }, - mode: 'x', - } - }, - datalabels: { - display: false, + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true + }, + mode: 'x', + } + }, + datalabels: { + display: false, + } } + } - - } - }); - setTimeout(function() { - window.timelineChartObject.zoom( 2-(1/(timeSpan/86400000)) ); + }); setTimeout(function() { - window.timelineChartObject.pan({x: -timeSpan}); - },100); - },500); + try { + window.timelineChartObject.zoom( 2-(1/(timeSpan/86400000)) ); + setTimeout(function() { + try { + window.timelineChartObject.pan({x: -timeSpan}); + } catch(e) { + + } + },500); + } catch(e) { + + } + },1500); + } // prepare Stats let htmlCost = ''; diff --git a/framework/public_pwa/assets/js/powerfox.js b/framework/public_pwa/assets/js/powerfox.js new file mode 100644 index 0000000..9e6fc7f --- /dev/null +++ b/framework/public_pwa/assets/js/powerfox.js @@ -0,0 +1,469 @@ +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register( + '/assets/js/3party/sw.js' + ); +} + +const app = async function(token) { + $('.appcontent').show(); + let minY=99999; + $.getJSON("/api/tariff/prices?token="+token, function(data) { + let chartLabels = []; + let chartData = []; + let chartColors = []; + for(let i=0;i { + return Math.round(value/totalConsumption*100) + '%'; + }, + } + } + } + }); + }); + + $.getJSON("/api/clearing/retrieve?meterId="+window.meterId+"&token="+token+crossbalance, function(data) { + let aggregationCost = {}; + let aggregationConsumption = {}; + let totalConsumption = 0; + let totalCost = 0; + let consumptionChart = []; + let costChart = []; + let oldEpoch = -1; + let timeSpan = Math.abs(data[0].endTime - data[data.length - 1].endTime); + + for(let i=0;i new Date( (point.x * 3600000)-demofy ).toLocaleString()); + const yValues = consumptionChart.map(point => (point.y/1000)); + const yValues2 = costChart.map(point => point.y); + + window.timelineChartObject = new Chart(ctxTimelineChart, { + type: 'line', + data: { + labels: xValues, // x-Werte als Labels + datasets: [{ + label: 'kWh', + yAxisID: 'A', + data: yValues, // y-Werte für die Datenpunkte + borderWidth: 1, + backgroundColor: '#273469', + borderColor: '#273469', + tension: 0.1 // Glättung der Linie + },{ + label: '€', + yAxisID: 'B', + data: yValues2, // y-Werte für die Datenpunkte + borderWidth: 1, + backgroundColor: '#606060', + borderColor: '#606060', + tension: 0.1 // Glättung der Linie + }] + }, + options: { + responsive: true, + scales: { + A: { + type: 'linear', + position: 'left', + ticks: { beginAtZero: true, color: '#273469' }, + // Hide grid lines, otherwise you have separate grid lines for the 2 y axes + grid: { display: false } + }, + B: { + type: 'linear', + position: 'right', + ticks: { beginAtZero: true, color: '#606060' }, + grid: { display: false } + }, + x: { ticks: { beginAtZero: true } } + }, + plugins: { + zoom: { + pan: { + mode: 'x', + enabled: true + }, + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true + }, + mode: 'x', + } + }, + datalabels: { + display: false, + } + } + + } + }); + setTimeout(function() { + window.timelineChartObject.zoom( 2-(1/(timeSpan/86400000)) ); + setTimeout(function() { + window.timelineChartObject.pan({x: -timeSpan}); + },100); + },500); + + // prepare Stats + let htmlCost = '
'; + htmlCost += ''; + htmlCost += ''; + htmlCost += ''; + htmlCost += '
⌀ Preis je kWh'+(totalCost/(totalConsumption/1000)).toFixed(3).replace('.',',')+'€
⌀ Kosten je Tag'+(totalCost/(timeSpan/86400000)).toFixed(2).replace('.',',')+'€
⌀ Kosten je Monat'+(totalCost/(timeSpan/(30*86400000))).toFixed(2).replace('.',',')+'€
'; + $('#statsCost').html(htmlCost); + + let htmlConsumption = ''; + htmlConsumption += ''; + htmlConsumption += ''; + htmlConsumption += ''; + htmlConsumption += '
⌀ kWh je Euro'+((totalConsumption/1000)/totalCost).toFixed(1).replace('.',',')+'
⌀ kWh je Tag'+((totalConsumption/1000)/(timeSpan/86400000)).toFixed(1).replace('.',',')+'
⌀ kWh je Monat'+((totalConsumption/1000)/(timeSpan/(30*86400000))).toFixed(1).replace('.',',')+'
'; + $('#statsConsumption').html(htmlConsumption); + }); + + $.getJSON("/api/access/settings?meterId="+window.meterId+"&token="+token, function(data) { + for (const [key, value] of Object.entries(data)) { + $(".meta_value_"+key).html(value); + $(".meta_visibility_"+key).show(); + } + }) + + $.getJSON("/api/access/getAssetMeta?meterId="+window.meterId+"&token="+token, function(data) { + if(typeof data.balancerule !== 'undefined') { + if(typeof data.balancerule.to !== 'undefined') { + $('.kosten').html("Einnahmen"); + $('.verbrauch').html("Einspeisung"); + } + } + let customName = window.meterId; + if(typeof data.operationMeta !== 'undefined') { + if(typeof data.operationMeta.meterPointName !== 'undefined') { + customName = data.operationMeta.meterPointName; + } + for (const [key, value] of Object.entries(data.operationMeta)) { + $(".meta_value_"+key).html(value); + $(".meta_visibility_"+key).show(); + } + } + if(typeof data.crossbalance !== 'undefined') { + $('.viewSelection').show(); + } + if(typeof data.clientMeta !== 'undefined') { + if(typeof data.clientMeta.meterPointName !== 'undefined') { + customName = data.clientMeta.meterPointName; + } + for (const [key, value] of Object.entries(data.clientMeta)) { + console.debug(".meta_value_"+key,value); + $(".meta_value_"+key).html(value); + $(".meta_visibility_"+key).show(); + } + } + + $('#meterPointName').html(customName); + $('.editable').editable().on('editsubmit', function (event, val) { + let dataToSend = { + token: token, + meterId: window.meterId + } + dataToSend[event.currentTarget.id] = val; + + $.ajax({ + type: 'POST', + url: '/api/access/updateAssetMeta', + data: JSON.stringify(dataToSend), + beforeSend: function (xhr) { + xhr.setRequestHeader('Authorization', 'Bearer '+token); + }, + contentType: 'application/json', + success: function(response) { + console.log(response); + }, + error: function(error) { + console.error(error); + } + }); + }); + }); +} + +$(document).ready(function() { + $.urlParam = function (name) { + var results = new RegExp('[\?&]' + name + '=([^&#]*)') + .exec(window.location.search); + + return (results !== null) ? results[1] || 0 : false; + } + + $('#meterId').val(window.localStorage.getItem("meterId")); + $('#token').val(window.localStorage.getItem("token")); + if($.urlParam('token')) { + $('#loginModal').modal('hide'); + $('#token').val($.urlParam('token')); + $('#meterId').val($.urlParam('meterId')); + window.meterId = $('#meterId').val(); + app($.urlParam('token')); + $('#loginModal').modal('hide'); + } else { + $('#loginModal').modal('show'); + } + if($.urlParam("crossbalance")) { + window.crossbalance = $.urlParam("crossbalance"); + } + + $(".viewSelect").click(function() { + if($(this).val() == "prosumer") { + window.crossbalance = true; + } else { + delete window.crossbalance; + } + app($.urlParam('token')); + }); + + $('#loginForm').submit(function(e) { + e.preventDefault(); + + $.ajax({ + type: 'POST', + url: '/api/powerfox/login', // replace with your endpoint URL + data: JSON.stringify({ email: $('#email').val(), password: $('#password').val()}), // replace with your data + contentType: 'application/json', + success: function(response) { + if(typeof response.token !== 'undefined') { + const token = response.token; + const meterId = response.meterId; + window.localStorage.setItem("token", token); + window.localStorage.setItem("meterId", meterId); + $('#meterId').val(meterId); + $('#token').val(token); + window.meterId = meterId; + location.replace("/?token="+token+"&meterId="+meterId); + /* For the moment we redirect to the standard login page as this might be the best option to avoid double implementation + app(token); + // Trigger Auto-Reload + setInterval(function() { app(token) }, 60000); + $('#loginModal').modal('hide'); + // + */ + } else { + console.error("Error login in"); + } + + }, + error: function(error) { + console.error(error); + } + }); + }) + + + +}) \ No newline at end of file diff --git a/framework/public_pwa/index.html b/framework/public_pwa/index.html index 36e26b6..13bfe06 100644 --- a/framework/public_pwa/index.html +++ b/framework/public_pwa/index.html @@ -185,7 +185,7 @@ + + + + +
+

Nutzung Kundenablesung - Powerfox/PowerOpti

+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/public_pwa/sitemap.xml b/framework/public_pwa/sitemap.xml index a00fab1..c99e774 100644 --- a/framework/public_pwa/sitemap.xml +++ b/framework/public_pwa/sitemap.xml @@ -3,4 +3,7 @@ / + + /powerfox.html + \ No newline at end of file diff --git a/framework/services/api-pwa.service.js b/framework/services/api-pwa.service.js index bec94de..4053eb5 100644 --- a/framework/services/api-pwa.service.js +++ b/framework/services/api-pwa.service.js @@ -46,7 +46,9 @@ module.exports = { "access.getAssetMeta", "access.settings", "access.sharedFolder", - "debit.open" + "debit.open", + "powerfox.login", + "httppull.updateReading" ], // Route-level Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares @@ -157,6 +159,14 @@ module.exports = { if(typeof ctx.params.req.query.token !== 'undefined') { auth = ctx.params.req.query.token; } + if(typeof ctx.params.req.body.email !== 'undefined') { + auth = { + meterId: 'powerfox', + email: ctx.params.req.body.email, + password: ctx.params.req.body.password + } + return auth; + } else if (auth) { let token = auth; if(auth.startsWith("Bearer")) { diff --git a/framework/services/httppull.service.js b/framework/services/httppull.service.js index b53e881..5c97461 100644 --- a/framework/services/httppull.service.js +++ b/framework/services/httppull.service.js @@ -67,25 +67,29 @@ module.exports = { } }, async handler(ctx) { - let json = await ctx.call("httppull.fetch",{requestId:ctx.params.requestId}); - if(json !== null) { - const results = await ctx.call("httppull_model.find",{ - query: { - requestId: ctx.params.requestId - } - }); - const rule = results[0].processor; - const Handlebars = require('handlebars'); - - function convertObject(obj, rulesTemplate) { - const template = Handlebars.compile(JSON.stringify(rulesTemplate)); - const context = { json: obj }; - const convertedData = JSON.parse(template(context)); - return convertedData; - } - - return convertObject(json, rule); - } else return null; + try { + let json = await ctx.call("httppull.fetch",{requestId:ctx.params.requestId}); + if(json !== null) { + const results = await ctx.call("httppull_model.find",{ + query: { + requestId: ctx.params.requestId + } + }); + const rule = results[0].processor; + const Handlebars = require('handlebars'); + + function convertObject(obj, rulesTemplate) { + const template = Handlebars.compile(JSON.stringify(rulesTemplate)); + const context = { json: obj }; + const convertedData = JSON.parse(template(context)); + return convertedData; + } + + return convertObject(json, rule); + } else return null; + } catch(e) { + return null; + } } }, updateReading: { @@ -99,7 +103,6 @@ module.exports = { meterId: ctx.params.meterId } }); - let json = await ctx.call("httppull.process",{requestId:results[0].requestId}); if(json !== null) { json.meterId = results[0].meterId; @@ -109,7 +112,7 @@ module.exports = { } else return {}; } } - }, + }, /** * Events diff --git a/framework/services/powerfox.service.js b/framework/services/powerfox.service.js new file mode 100644 index 0000000..80f8e72 --- /dev/null +++ b/framework/services/powerfox.service.js @@ -0,0 +1,219 @@ +"use strict"; +/** + * Service to do http pull requests to fetch meter readings from external REST APIs + */ + +/** + * @typedef {import('moleculer').ServiceSchema} ServiceSchema Moleculer's Service Schema + * @typedef {import('moleculer').Context} Context Moleculer's Context + */ +const ApiGateway = require("moleculer-web"); // Included for Invalid Authentication Errors +const axios = require("axios"); +let db; + +/** @type {ServiceSchema} */ +module.exports = { + name: "powerfox", + + /** + * Dependencies + */ + dependencies: ["httppull_model"], + + /** + * Actions + */ + actions: { + + login: { + rest: { + method: "POST", + path: "/login" + }, + async handler(ctx) { + const textLoginResp = await axios.get("https://backend.powerfox.energy/api/2/my/all/devices",{ + auth: { + username: ctx.params.email, + password: ctx.params.password + } + }); + + let mainDeviceId = null; + for(let i=0;i