D3 und Aurelia
Im letzten Post habe ich gezeigt, wie man auf einen ioBroker Datenpunkt von aussen mit einem einfachen Javascript-Progrämmchen zugreifen kann. Jetzt wollen wir die Grundlagen für ein etwas grösseres Projekt legen. Als Basis verwende ich gern das moderne JavaScript Framework Aurelia.
Wir beginnen mit dem Aurelia-skeleton/typescript, das man hier herunterladen kann. Nach dem Entpacken benötigen Sie nur das Unterverzeichnis 'skeleton-typescript'. Ausserdem müssen npm und jspm installiert sein. Das Readme des Aurelia Skeleton erläutert die Installation recht gut.
Gehen Sie in das Verzeichnis und geben Sie ein:
npm install
jspm install
npm install d3
Dann ändern wir nur die Datei src/app.ts und erstellen zwei neue Dateien: solar.html und solar.ts. Das ist alles, um anzufangen.
In app.ts fügen wir die unten gelb unterlegte Zeile ein, um einen neuen Menüpunkt zur Demo hinzuzufügen:
solar.html ist recht einfach:
Die eigentliche Logik steckt in solar.ts:
Starten Sie den Entwicklungsserver, indem Sie 'gulp watch' eingeben. Wenn Sie dann Ihren Browser auf localhost:9000 richten, sollten Sie ungefähr folgendes sehen:
Wir beginnen mit dem Aurelia-skeleton/typescript, das man hier herunterladen kann. Nach dem Entpacken benötigen Sie nur das Unterverzeichnis 'skeleton-typescript'. Ausserdem müssen npm und jspm installiert sein. Das Readme des Aurelia Skeleton erläutert die Installation recht gut.
Gehen Sie in das Verzeichnis und geben Sie ein:
npm install
jspm install
npm install d3
Dann ändern wir nur die Datei src/app.ts und erstellen zwei neue Dateien: solar.html und solar.ts. Das ist alles, um anzufangen.
In app.ts fügen wir die unten gelb unterlegte Zeile ein, um einen neuen Menüpunkt zur Demo hinzuzufügen:
1 import {Router, RouterConfiguration} from 'aurelia-router'; 2 3 export class App { 4 public router: Router; 5 6 public configureRouter(config: RouterConfiguration, router: Router) { 7 config.title = 'Aurelia'; 8 config.map([ 9 { route: ['', 'welcome'], name: 'welcome', moduleId: 'welcome', nav: true, title: 'Welcome' }, 10 { route: 'users', name: 'users', moduleId: 'users', nav: true, title: 'Github Users' }, 11 { route: 'child-router', name: 'child-router', moduleId: 'child-router', nav: true, title: 'Child Router' }, 12 { route: 'solar', name: 'solar', moduleId: 'solar', nav: true, title: 'Solaranlage'} 13 ]); 14 15 this.router = router; 16 } 17 }
solar.html ist recht einfach:
1 <template> 2 <div class="container" style="border-color: blue;border-width:2px;border-style:solid"> 3 <h2>Solaranlage</h2> 4 <div class="row"> 5 <div class="col-sm-4"> 6 <h3>Tagesenergie</h3> 7 ${day_energy} kWh 8 </div> 9 <div class="col-sm-4"> 10 <h3>Jahresenergie</h3> 11 ${year_energy} kWh 12 </div> 13 <div class="col-sm-4"> 14 <h3>Gesamt</h3> 15 ${total_energy} MWh 16 </div> 17 </div> 18 <div id="graph" class="area"> 19 <h3>Momentane Leistung</h3> 20 21 </div> 22 </div> 23 </template>
Die eigentliche Logik steckt in solar.ts:
1 import * as d3 from 'd3'; 2 import 'fetch'; 3 4 const padding = 25; 5 const lower_limit = 0; 6 const height = "80px"; 7 const server = "192.168.16.140:8087"; 8 const ACT_POWER = "fronius.0.powerflow.P_PV"; 9 const DAY_ENERGY = "fronius.0.inverter.1.DAY_ENERGY" 10 const YEAR_ENERGY = "fronius.0.inverter.1.YEAR_ENERGY" 11 const TOTAL_ENERGY = "fronius.0.inverter.1.TOTAL_ENERGY" 12 13 14 const format = d3.format(".2f") 15 16 export class Solar { 17 private act_power = 0 18 private day_energy = 0 19 private year_energy = 0 20 private total_energy = 0 21 private svg 22 private svgAxis 23 private xAxis 24 private x 25 private width 26 private resizing = false 27 28 private update_dimensions() { 29 if (!this.resizing) { 30 this.resizing = true 31 let bounds = d3.select("#graph") 32 .node().getBoundingClientRect() 33 34 this.width = bounds.width - 2 * padding 35 this.svg.attr("width", this.width + padding) 36 37 this.x = d3.scaleLinear().range([padding, this.width]) 38 .domain([lower_limit, 10000]); 39 this.xAxis.scale(this.x); 40 this.svgAxis 41 .attr("transform", "translate(0,50)") 42 .call(this.xAxis); 43 this.resizing = false 44 } 45 } 46 47 attached() { 48 this.svg = d3.select("#graph").append("svg") 49 .attr("width", "90%") 50 .attr("height", height) 51 .classed("area", false) 52 53 this.xAxis = d3.axisBottom().tickFormat(d => { 54 return this.x.tickFormat(12, d3.format(",d"))(d) 55 }) 56 57 this.svgAxis = this.svg.append("g").classed("x axis", true) 58 this.update_dimensions() 59 60 const bar = this.svg.append("rect") 61 .attr("id", "power_bar") 62 .attr("x", this.x(0)) 63 .attr("y", 0) 64 .attr("height", "40px") 65 .attr("fill", "yellow") 66 .attr("stroke", "blue") 67 68 const text = this.svg.append("text") 69 .attr("id", "textval") 70 .attr("x", padding + 5) 71 .attr("y", 25) 72 73 setInterval(() => { 74 this.update() 75 }, 10000) 76 77 window.addEventListener("resize", () => { 78 this.update_dimensions() 79 }) 80 this.update() 81 82 } 83 84 async update() { 85 const result = await fetch(`http://${server}/get/${ACT_POWER}`) 86 const power = await result.json() 87 this.update_dimensions() 88 89 let bar = d3.select("#power_bar") 90 bar.attr("width", this.x(power.val) - this.x(0)) 91 let text = d3.select("#textval") 92 text.text(`${power.val} Watt`) 93 const day = await 94 (await fetch(`http://${server}/get/${DAY_ENERGY}`)).json() 95 this.day_energy = format(day.val / 1000) 96 const year = await 97 (await fetch(`http://${server}/get/${YEAR_ENERGY}`)).json() 98 this.year_energy = format(year.val / 1000) 99 const total = await 100 (await fetch(`http://${server}/get/${TOTAL_ENERGY}`)).json() 101 this.total_energy = format(total.val / 1000000) 102 } 103 104 }
Starten Sie den Entwicklungsserver, indem Sie 'gulp watch' eingeben. Wenn Sie dann Ihren Browser auf localhost:9000 richten, sollten Sie ungefähr folgendes sehen:
Abgesehen von ein wenig "drumrum" für die Integration in eine noch zu bauende Aurelia WebApp, ist das eigentlich weitgehend dasselbe, wie im vorherigen Teil gezeigt. Insbesondere der Code in "attached()" und "update()" sollte Ihnen bekannt vorkommen. attached() ruft das Framework dann auf, wenn unsere Seite ans DOM gebunden ist. Update wird wie gehabt alle 10 Sekunden aufgerufen. Die neue Funktion update_dimensions() dient dazu, die Grössen anzupassen, wenn der Anwender das Fenster ändert oder das Handy von Horizontal nach Vertikal dreht.
Ach ja, und die Programmiersprache ist jetzt TypeScript. Das ist nicht so arg weit weg von ECMA 6 und sollte deshalb verständlich sein. In update() arbeiten wir jetzt mit dem Standard-Fetch-Api anstelle der d3-eigenen Datenbeschaffungs-Funktionen.
Das "await" Schlüsselwort in den ersten beiden Zeilen von update() ist "syntactic sugar" um Promises und dient dazu, das UI nicht einfrieren zu lassen, während man auf die Ergebnisse länger laufender Funktionen wartet. Bei weitergehendem Interesse finden Sie dazu viel Dokumentation unter "asynchrones Programmieren" und "Javascript Promise".
Kommentare
Kommentar veröffentlichen